API reference
A JSON REST API over HTTPS. One error shape, one job envelope, and a stable error.code on everything that can go wrong.
Base URL: https://gateway.market4me.ai//api/v1v1.0.0
Authentication
Every request needs an API key. Create one in the dashboard under Settings → API & integrations. The key is shown once — we store only a SHA-256 hash and genuinely cannot recover it, so if you lose it, revoke it and make another.
# preferred
curl https://gateway.market4me.ai//api/v1/me -H "Authorization: Bearer m4m_live_..."
# equivalent, for clients that only support an API-key header
curl https://gateway.market4me.ai//api/v1/me -H "X-API-Key: m4m_live_..."Keys are prefixed m4m_live_ or m4m_test_. A key inherits exactly the access of the person who created it — the same workspace, the same role — and can never exceed it.
A key spends real credits. m4m_test_ keys are not a sandbox: they bill identically and exist only so you can revoke a staging credential without touching production. Treat every key like a password, and give autonomous agents a key with a spending ceiling.
Scopes
A key created with no scopes has full access — the common case. Selecting scopes makes it a strict allow-list, which is what you want for a key you hand to a third party or an agent.
An unrecognised scope is rejected with 400 rather than ignored. A typo like jobs:list must never quietly produce an all-powerful key.
| Scope | Grants |
|---|---|
brands:read | Read brands and their brand intelligence |
brands:write | Create and update brands, trigger analysis |
generate:video | Generate video (charges credits) |
generate:image | Generate and edit images (charges credits) |
generate:audio | Generate voiceover and clone voices (charges credits) |
generate:content | Generate scripts, captions, carousels, strategy |
jobs:read | Read job status and results |
assets:read | List generated assets and available models |
assets:write | Mint upload URLs to put your own media into the account |
credits:read | Read credit balance, pricing and usage |
analytics:read | Read performance analytics |
webhooks:read | List webhook endpoints and their delivery health |
webhooks:write | Create and delete webhook endpoints |
GET /v1/scopes tells you which scopes the calling key actually holds.
Errors
Every failure returns the same shape. Branch on error.code — it is stable. error.message is written to be read by a person or a model and says what to do next, but it is not a contract.
{
"error": {
"type": "insufficient_credits_error",
"code": "insufficient_credits",
"message": "This operation costs 350 credits but the account has 120. Top up at …/dashboard/billing.",
"credits_required": 350,
"credits_available": 120,
"request_id": "req_8f21c0…",
"doc_url": "https://market4me.ai/docs/api#insufficient_credits"
}
}Codes worth handling explicitly
| HTTP | code | What to do |
|---|---|---|
| 401 | missing_api_key | No key was sent. Add the Authorization header. |
| 401 | invalid_api_key | The key is wrong or unknown. Do not retry. |
| 401 | revoked_api_key | Someone revoked it. Issue a new one. |
| 403 | insufficient_scope | The key lacks the scope this route needs. Create a key with it — you cannot widen an existing key. |
| 400 | missing_required_parameter | Read error.param for which one. |
| 400 | invalid_enum | The message lists the allowed values. |
| 400 | brand_not_analysed | Run POST /brands/{id}/analyze, wait for complete, retry. |
| 402 | insufficient_credits | Not enough balance. Retrying cannot succeed — tell the user. |
| 402 | subscription_required | Buying credits will not fix this. The endpoint needs an active subscription. |
| 402 | key_credit_limit_reached | This key hit its own spending ceiling. Raise it or use another key. |
| 409 | idempotency_key_reused | Same key, different body. Use a new key for a new request. |
| 429 | rate_limit_exceeded | Back off for Retry-After seconds. |
| 502 | generation_failed | The generation failed and credits were refunded. Safe to retry once. |
| 404 | not_found | Missing — or not visible to this key. The two are deliberately indistinguishable. |
Every response carries an X-Request-Id header, echoed as error.request_id. Quote it in support requests.
Rate limits
Limits are per key, per minute — not per IP, so your server-to-server integration is not throttled by traffic from elsewhere in your company. The default is 120 requests/minute and you can set it per key when you create it.
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window. |
X-RateLimit-Remaining | Requests left in it. |
X-RateLimit-Reset | Unix timestamp when the window resets. |
Retry-After | On a 429 only — seconds to wait. |
Rate limits are about fairness, not billing. Credits are the billing control, and a rate limit never charges you.
Idempotency
Send an Idempotency-Key header on any POST. Replaying the same key returns the original response instead of doing the work again — so a timeout, a retry or a duplicated queue message cannot charge you twice.
curl https://gateway.market4me.ai//api/v1/videos \
-H "Authorization: Bearer m4m_live_..." \
-H "Idempotency-Key: 7f3c1e90-2b44-4a1e-9d77-8c0e2a1b5f60" \
-H "Content-Type: application/json" \
-d '{"prompt":"a neon sign flickering on at dusk"}'- Use a fresh UUID per logical operation, and reuse it across retries of that operation.
- A replayed response carries
Idempotent-Replay: true. - Same key with a different body is a
409— that is a bug on the caller's side, and answering with the wrong cached response would be worse than failing. - Keys are remembered for 24 hours, and are scoped to the API key that used them.
- Only successful responses are cached. A failure can always be retried with the same key.
Jobs
Short generations (clips, images, speech, avatars) return the finished asset directly. Long ones — /brand-videos, /films — return a job, and every job in the API looks the same regardless of which engine is doing the work.
{
"id": "job_9f2c1a7e4b…",
"object": "job",
"kind": "brand_video",
"status": "running", // queued → running → succeeded | failed | cancelled
"progress": 45,
"credits": 120,
"output": null, // populated once succeeded
"error": null, // why it failed; credits are refunded automatically
"metadata": { "your": "data" },
"poll_url": "https://gateway.market4me.ai//api/v1/jobs/job_9f2c1a7e4b…",
"is_terminal": false,
"created_at": "2026-07-21T10:04:11Z",
"completed_at": null
}On success, output.url is the finished asset — a stable, shareable link you can hand straight to a user. output.download_url is a direct download that expires after 24 hours.
Waiting for a job
Three ways, best first:
- Webhooks — be told. Best for a real integration. See below.
POST /jobs/{id}/wait— one blocking call that returns as soon as the job is terminal (up totimeout_s, max 300). Ideal for scripts and AI agents, which have no reliable timer of their own.- Polling
GET /jobs/{id}— works, but you are writing a loop that the other two make unnecessary.
# blocks until finished, or returns timed_out:true so you can call again
curl -X POST https://gateway.market4me.ai//api/v1/jobs/job_9f2c1a7e4b/wait \
-H "Authorization: Bearer m4m_live_..." \
-H "Content-Type: application/json" \
-d '{"timeout_s": 120}'GET /jobs/{id} also accepts the raw UUID of work started in the Market4Me dashboard, so a user can hand you a render id and you can track it.
Webhooks
Register an endpoint and Market4Me POSTs to it when a job finishes, instead of you polling.
curl https://gateway.market4me.ai//api/v1/webhooks \
-H "Authorization: Bearer m4m_live_..." \
-H "Content-Type: application/json" \
-d '{"url":"https://yourapp.com/hooks/market4me","events":["job.succeeded","job.failed"]}'The response contains a secret — shown once. Store it; you need it to verify deliveries.
Events
| Event | Fires when |
|---|---|
job.running | A job started processing |
job.succeeded | A job finished successfully and its output is ready |
job.failed | A job failed (credits are refunded automatically) |
brand.analysis.completed | Brand analysis finished and brand intelligence is available |
Omit events to receive everything.
Verifying a delivery
Each request carries M4M-Signature: t=<unix>,v1=<hex>, where the digest is HMAC-SHA256 over {timestamp}.{raw body}. Check the digest and the age — the timestamp is inside the signed payload precisely so a captured request cannot be replayed later.
import crypto from 'node:crypto'
// IMPORTANT: verify against the RAW body, before any JSON parsing.
export function verify(rawBody, header, secret) {
const t = /t=(\d+)/.exec(header)?.[1]
const v1 = /v1=([a-f0-9]+)/.exec(header)?.[1]
if (!t || !v1) return false
// Reject anything older than 5 minutes.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
const expected = crypto.createHmac('sha256', secret)
.update(`${t}.${rawBody}`).digest('hex')
// Constant-time compare — a plain === leaks the digest byte by byte.
return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
}import hmac, hashlib, time, re
def verify(raw_body: bytes, header: str, secret: str) -> bool:
t = re.search(r"t=(\d+)", header)
v1 = re.search(r"v1=([a-f0-9]+)", header)
if not t or not v1:
return False
if abs(time.time() - int(t.group(1))) > 300:
return False
expected = hmac.new(
secret.encode(),
f"{t.group(1)}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(v1.group(1), expected)Delivery behaviour
- Answer 2xx within 10 seconds. Do the real work asynchronously.
- Failures retry 6 times with backoff over roughly 2 hours.
- An endpoint that fails 20 times consecutively is disabled — check
GET /webhooksif deliveries stop. - Redirects are not followed. Register the final URL.
- Endpoints must be public
https. Private, loopback and link-local addresses are rejected, and re-checked before every delivery. POST /webhooks/{id}/testsends a test event so you can verify your endpoint without spending credits on a real render.
Deliveries are at-least-once. Every event carries a unique id — de-duplicate on it rather than assuming exactly-once.
Models
Generation endpoints take an optional model, expressed as a quality/price tier. Omit it and you get a sensible default — most callers should.
| Tier | For |
|---|---|
economy | Drafts, tests, high-volume work. Cheapest. |
fast | Quick turnaround at a moderate price. |
standard | The balanced default for most marketing video. |
premium | Higher fidelity motion and detail. |
ultra | Best general-purpose quality. |
max | 4K output. Most expensive. |
Images use economy, standard and premium. GET /v1/models returns the live catalogue with per-tier prices, so you can pick on cost rather than guessing.
All endpoints
Generated from the OpenAPI spec this API actually serves, so it cannot drift from the implementation.
Account
Call this FIRST. It verifies the key and returns everything needed to plan work: credit balance, subscription plan, which brands exist (with their analysis status), what this key is allowed to do, and which endpoints are gated behind a subscription.
Credit balance and recent transactions
| Parameter | Type | Notes |
|---|---|---|
limitquery | integer |
Returns the exact credit cost of an operation and whether the account can afford it. Use this before any generation you have not priced before, and to compare options (a 1080p 10s clip vs a 720p 5s one) without spending anything. Free.
| Parameter | Type | Notes |
|---|---|---|
operation | string | One of: video image speech avatar motion_sync ugc brand_video content |
duration_s | number | |
quality | string | |
model | string | |
mode | string | |
text | string | For operation=speech — cost scales with length. |
operations | array | Price several at once and get a total. Use this to budget a whole campaign in one call. |
API usage and spend, grouped by endpoint
| Parameter | Type | Notes |
|---|---|---|
daysquery | integer |
Brands
List brands
Creates a brand. If `website_url` is given, brand analysis starts immediately — it crawls the site and builds the brand brain that makes later generations on-brand. Analysis takes 30–90s; poll GET /v1/brands/{id} until analysis_status is "complete".
| Parameter | Type | Notes |
|---|---|---|
namerequired | string | |
website_url | string | Strongly recommended — this is what makes generations on-brand. |
niche | string | |
description | string |
Get one brand
Update a brand
| Parameter | Type | Notes |
|---|---|---|
name | string | |
website_url | string | |
niche | string | |
description | string | |
tone_guidelines | string | |
target_audience | string | |
products_services | string |
Re-crawls the brand website and rebuilds the brand brain. Use after the site changes, or if analysis_status is "failed". Runs in the background — poll GET /v1/brands/{id}.
The structured understanding of the business: tone of voice, audience, products, USP, brand story, key messages, content pillars and themes. Useful when you want to reason about the brand yourself (writing copy, planning a campaign) rather than relying on automatic grounding.
Generation
The simplest useful call in the API. Returns the finished video URL synchronously — no polling. Automatically grounded in the brand (set use_brand:false to opt out). For a multi-scene narrated film use POST /films; for the full brand pipeline with scripted voiceover and captions use POST /brand-videos.
| Parameter | Type | Notes |
|---|---|---|
promptrequired | string | What should happen on screen. Describe the shot, not the marketing goal. |
brand_id | string | Omit to use the default workspace brand. |
duration_s | number | Cost scales with this. |
aspect_ratio | string | One of: 9:16 16:9 1:1 4:5 3:4 4:3 |
quality | string | One of: 480p 720p 1080p 4k |
model | string | Quality/price tier. Omit for the default. GET /models lists them with prices. One of: economy fast standard premium ultra max |
image_url | string | Animate this image instead of generating from scratch. |
image_urls | array | Up to 9 reference images. |
use_brand | boolean | |
metadata | object |
Synchronous. mode=generate makes a new image from a prompt; mode=edit changes an existing one (needs image_url); mode=face-swap places a face into a scene (needs image_url and face_image_url). Output URLs can be fed straight into POST /videos as image_url.
| Parameter | Type | Notes |
|---|---|---|
prompt | string | |
mode | string | One of: generate edit face-swap |
brand_id | string | |
image_url | string | |
face_image_url | string | |
quality | string | One of: low medium high |
model | string | Quality/price tier. Omit for the default. One of: economy standard premium |
size | string | |
use_brand | boolean |
Generates a voiceover audio file. Cost scales with character count. The returned URL can be passed to POST /avatars as audio_url.
| Parameter | Type | Notes |
|---|---|---|
textrequired | string | |
voice | string | Voice id. Omit for the brand default. |
brand_id | string |
Takes a portrait image plus words and returns a video of that person saying them, lip-synced. Give either `text` (we generate the voice) or `audio_url` (your own track).
| Parameter | Type | Notes |
|---|---|---|
image_urlrequired | string | Portrait of the person who should speak. |
text | string | |
audio_url | string | |
voice | string | |
resolution | string | One of: 480p 720p 1080p |
brand_id | string |
Brand-grounded copy. content_type short_video/reel/tiktok returns a script with hooks; carousel returns slides; caption returns a post caption with hashtags. Cheap and fast — use it to draft before spending credits on a render.
| Parameter | Type | Notes |
|---|---|---|
content_typerequired | string | One of: short_video reel tiktok carousel caption |
topic | string | |
platforms | array | |
custom_instructions | string | |
slides_count | integer | |
brand_id | string |
The complete marketing-video pipeline, not a single clip. Give a `topic` and it writes the script itself. ASYNCHRONOUS: returns a job — poll GET /jobs/{id} or call POST /jobs/{id}/wait. Requires the brand to have been analysed.
| Parameter | Type | Notes |
|---|---|---|
topic | string | What the video is about. We write the script. |
script | string | Your own script, instead of a topic. |
brand_id | string | |
duration_s | number | |
platform | string | One of: tiktok instagram youtube linkedin facebook |
aspect_ratio | string | |
metadata | object |
The most capable generation in the product: an idea becomes a storyboarded, multi-scene, narrated film. ASYNCHRONOUS — returns a job. REQUIRES AN ACTIVE SUBSCRIPTION; buying credits does not unlock it, so a 402 with code subscription_required cannot be fixed by topping up.
| Parameter | Type | Notes |
|---|---|---|
idearequired | string | |
duration_s | number | |
aspect_ratio | string | |
style | string | One of: cinematic cartoon |
voiceover | boolean | |
brand_id | string |
Writes a multi-scene creator-style ad script. FREE — no credits. Iterate here with the user, then pass the scenes to POST /ugc to render.
| Parameter | Type | Notes |
|---|---|---|
product | string | |
angle | string | |
ugc_type | string | One of: testimonial unboxing demo review story |
duration_s | number | |
brand_id | string |
Render a UGC ad from a scene script
| Parameter | Type | Notes |
|---|---|---|
scenesrequired | array | |
image_url | string | The creator/actor portrait. |
quality | string | One of: 480p 720p 1080p |
brand_id | string |
Transcribe a video, or burn subtitles into it
| Parameter | Type | Notes |
|---|---|---|
video_urlrequired | string | |
mode | string | One of: transcript subtitles |
brand_id | string |
Jobs
List jobs
| Parameter | Type | Notes |
|---|---|---|
statusquery | string | One of: queued running succeeded failed cancelled |
kindquery | string | |
brand_idquery | string | |
limitquery | integer |
Poll until is_terminal is true. On success, `output.url` is the finished asset. Prefer POST /jobs/{id}/wait, which blocks instead of making you poll.
Long-polls, up to timeout_s (default 60, max 300). Returns as soon as the job is terminal. If it is still running when the timeout hits you get 200 with timed_out:true — call again to keep waiting. This is the preferred way to await a job: no polling loop, no wasted turns.
| Parameter | Type | Notes |
|---|---|---|
timeout_s | integer |
Assets
Everything this account has produced. This is where a render that outlived its request window turns up.
| Parameter | Type | Notes |
|---|---|---|
kindquery | string | One of: video image audio |
brand_idquery | string | |
limitquery | integer |
Two steps: call this, then PUT the bytes to `upload_url` with the same Content-Type. Pass the returned `asset_url` to any endpoint that takes image_url / video_url / audio_url. This is how you bring a product photo or an existing clip into a generation.
| Parameter | Type | Notes |
|---|---|---|
filenamerequired | string | |
content_typerequired | string | |
brand_id | string |
List available generation models
Webhooks
List webhook endpoints
Be told when a job finishes instead of polling. The signing secret is returned ONCE — store it. Each delivery carries `M4M-Signature: t=<unix>,v1=<hmac-sha256 of "t.body">`.
| Parameter | Type | Notes |
|---|---|---|
urlrequired | string | https required (http allowed for localhost). |
events | array | Omit for all events. |
description | string |
Delete a webhook endpoint
Verifies the endpoint is reachable and answers 2xx, without spending credits on a real render first.
List the event types you can subscribe to
Driving this from an AI agent instead? The MCP server wraps all of this as tools for Claude, ChatGPT and Cursor.