Register a mediaWebhookUrl, receive a POST when a job finishes, poll GET /api/media/jobs/{id}, handle error codes, and understand charging and idempotency limits.
Operational guide to asynchronous media jobs: webhook registration, statuses, polling, error codes, charges and idempotency limits — strictly per the current implementation in the code.
Job lifecycle
POST /api/media/generate — on success 202 and { ok: true, jobId, status: "processing" }. There are no separate queued / running statuses in the API: until the job finishes, the client sees processing.
Upstream may go through internal stages (waiting, generating) — these are not reflected in the public API; while polling, the job stays processing.
Terminal statuses: success (has resultUrls, charged on success) or fail (no charge).
Completion is recorded once: via the upstream callback or on a lazy poll in GET /api/media/jobs/{id}.
Polling: GET /api/media/jobs/{id}
GEThttps://getvibeapi.com/api/media/jobs/{id}
Parameter
Type
Description
status
string
submitting | processing | success | fail. submitting + media_submission_uncertain is non-terminal; within about 10–15 minutes such a submission is closed as fail (media_submission_not_started) without a charge — create a new task.
resultUrls
string[]
Signed URLs /api/media/file/{id}/{idx}?sig=… (no API key).
priceRub
number
Charge amount in USD (0 on fail or before completion).
failMsg
string | null
Safe message on fail or a non-terminal media_submission_uncertain.
errorCode
string | null
Error code (media_generation_failed) or non-terminal media_submission_uncertain.
retryGuidance
string | null
On media_submission_uncertain: a safe instruction to poll the same jobId later and not create a duplicate automatically.
# 2. poll the job until it is ready
curl https://getvibeapi.com/api/media/jobs/JOB_ID \
-H "Authorization: Bearer $GETVIBE_API_KEY"
# → {"status":"success","resultUrls":["https://..."],"priceRub":...}
Recommended polling interval: 3–5 s, with an exponential backoff up to 30 s. The client-side timeout is per your application SLA (video generation can take minutes).
Webhook registration
GEThttps://getvibeapi.com/api/webhooks
PATCHhttps://getvibeapi.com/api/webhooks
One URL per account: mediaWebhookUrl. Configured in dashboard → Webhooks or via the API with an account session (a cookie after login). The endpoint does not accept sk-pv-… — only an authenticated session.
# Example (a dashboard session is required, not an API key)
curl -X PATCH https://getvibeapi.com/api/webhooks \
-H "Content-Type: application/json" \
-b "session=YOUR_SESSION" \
-d '{"mediaWebhookUrl":"https://example.com/webhooks/media"}'
# Clear the URL
curl -X PATCH https://getvibeapi.com/api/webhooks \
-H "Content-Type: application/json" \
-b "session=YOUR_SESSION" \
-d '{"mediaWebhookUrl":null}'
URL requirements
Parameter
Type
Description
mediaWebhookUrl
HTTPS URL
A public HTTPS address; private/loopback addresses and our own domains are refused (SSRF protection), and the address is re-checked before every delivery. An empty string or null resets it.
balanceWebhookUrl
HTTPS URL
A separate webhook for low balance (not media), signed with the same account secret, x-pv-event: balance.low. The threshold is balanceWebhookThresholdRub.
Webhook delivery on job completion
When a job reaches a terminal status, GetVibe creates one logical event and delivers it via POST to the registered mediaWebhookUrl. Delivery is capped: the service makes up to 5 attempts total. The same payload may arrive more than once.
// POST to your mediaWebhookUrl when a job finishes
{
"jobId": "...",
"status": "success", // or "fail"
"model": "veo-3.1",
"kind": "video", // image | video | audio | music | 3d
"resultUrls": ["https://getvibeapi.com/api/media/file/..."],
"failMsg": null,
"createdAt": "2026-06-17T10:00:00.000Z"
}
Delivery behavior (as in code)
Parameter
Type
Description
Signature / auth
—
Every delivery is signed with Standard Webhooks: the webhook-id, webhook-timestamp and webhook-signature headers (v1, base64 HMAC-SHA256 of “id.timestamp.body”). The account secret is shown in the dashboard → Webhooks and returned to an API key as accountSecret by GET /api/media/webhook-secret; deliveries to a per-request webhook_url are signed with that key's secret (the secret field). Reject a request with a wrong signature or a timestamp older than 5 minutes.
Retries
outbox
Up to 5 attempts total; interval after a failure: 1, 2, 4, 8 minutes. After exhausting attempts, the event moves to FAILED and is no longer delivered automatically. Polling GET /api/media/jobs/{id} remains the source of state.
Receiver dedup
required
Deduplicate by the jobId + status pair and make your handler idempotent. Do not use the webhook as the sole source of state.
Content-Type
application/json
Body is JSON with fields jobId, status, model, kind, resultUrls, failMsg, createdAt.
POST /api/media/generate errors
HTTP
When
Charge
Retry
202
Job accepted (ok: true, jobId)
No (only after success)
—
400
Invalid JSON / request body
No
Fix the request
401
Missing or invalid API key
No
Check Authorization
402
Insufficient balance (admission)
No
Top up the balance
422
Unknown model, invalid opts, ok: false from facade
No
Fix model/opts
502
media_submit_failed — upstream unavailable
No
Retry with backoff
503
priceUnavailable, routing guard, VIDEO_DISABLED, veo without price
No
Another model / later
Polling errors and terminal failures
HTTP / status
Meaning
Charge
404
job not found or a foreign jobId
No
processing
The job is still running
No
success
Done, resultUrls populated
Yes — priceRub in the response
fail
Generation failed
No (priceRub = 0)
Billing and idempotency
Charged once on success. In finalizeJob the transition processing → success|fail is atomic: a repeated callback or poll does not create a second charge.
fail — free. No debit is performed on a fail status.
Admission quote. If a quote was saved at creation, exactly it is charged on success (never more than the actual upstream metering).
Create idempotency — by header. A repeated POST to /api/media/generate without the header creates a new job and a new charge on success. Send Idempotency-Key (up to 255 ASCII characters): a retry with the same key and body within 24 hours returns the same jobId without a new job; the same key with a different body is 422, and 409 while the first request is still being processed. The jobId of a paid generation is also shown in GET /v1/generations (the job_id field).
Webhook is capped at five attempts. One logical event is created per terminal transition, but delivery attempts may repeat. After five failures the event gets the status FAILED . The client handler must deduplicate the pair jobId + status.