Skip to main content

Webhooks

Pass a webhook_url when you submit a job and we'll POST to it when the job finishes, instead of you having to poll. The webhook is configured per request: each job carries its own destination, so you can route clips to different endpoints for different purposes. A job submitted without a webhook_url sends no webhook.


When it fires

Once per job outcome, whichever way it goes:

  • job.completed — the clip rendered and is ready.
  • job.failed — the job will never produce a clip (a rejected demo_url, an unplayable demo, a render failure, or a download that we retried until we gave up). error_code tells you which.

Nothing fires while a job is still on its way. A transient failure we intend to retry — a slow or dropped demo download — is not an outcome, so it stays silent; you'll get job.completed if a later attempt works, or job.failed once we stop trying. Either way you hear from us exactly once per attempt at the job.

There is no "submitted" event. The synchronous { "job_id": "..." } response from POST /api/clips is your confirmation that the job was accepted.

If you re-run a job — either with force: true or by resubmitting it — it gets a fresh webhook for its new outcome, and we cancel the previous outcome's webhook if it hasn't been delivered yet.


Request

  • POST to the webhook_url supplied on the job.
  • Content-Type: application/json.
  • 10 second timeout per attempt.
  • Optional Authorization header. Pass webhook_auth_header alongside webhook_url when you submit the job to have that value sent as the Authorization header on the webhook request. It's forwarded verbatim — set it to whatever your endpoint expects (e.g. Bearer <token> or Basic <credentials>) and use it to authenticate the request on your end. Omit it and no Authorization header is sent. Sending webhook_auth_header without a webhook_url returns a 400.
  • Not signed. There is no header proving a request originated from dinkem.gg. If you need to verify authenticity, use webhook_auth_header above, or cross-check job_id/clip_id against values you already hold from your own POST /api/clips calls.

Headers

HeaderDescription
X-Dinkem-Delivery-IdUnique ID for this webhook. Stable across every retry of the same webhook — this is what you deduplicate on.
X-Dinkem-AttemptWhich attempt this is, starting at 1.

Retries

Respond 2xx to accept a webhook. Anything else — any non-2xx status, a timeout, a connection error — is treated as a failed delivery and retried.

  • 10 attempts total: the first immediately, then 9 more 15 minutes apart. After that we stop and the webhook is never delivered — though you can trigger a fresh delivery on demand with POST /api/jobs/:id/resend-webhook.
  • Every non-2xx is retried, including 4xx. We don't try to distinguish "your endpoint is misconfigured" from "your endpoint is briefly down mid-deploy" — a 404 during a rollout gets the same treatment as a 503.

Respond quickly and do your real work asynchronously. If you need longer than 10 seconds to process a clip, return 2xx first and queue it on your side; a slow endpoint reads as a failure and gets retried, which usually means processing the same clip twice.

Delivery is at-least-once

You may receive the same webhook more than once, so make your handler idempotent and dedupe on X-Dinkem-Delivery-Id. This isn't rare or theoretical: if your 2xx is lost in transit, or arrives after our 10 second timeout, we can't tell that apart from a failure and will retry a webhook you already processed successfully.

Delivery can be out of order

Webhooks are not ordered, and a retry schedule spanning two hours makes that visible. If you redo a job while an earlier outcome is still being retried, that older webhook can still land after the newer one — for example a job.failed arriving after the job.completed that superseded it. We cancel undelivered webhooks when you redo a job, but one already in flight will still arrive.

Treat the job's current state as the truth, not the last webhook you received. If ordering matters to you, fetch the job (or the clip) and confirm before acting on a failure.


Payload

Both events share one shape, so you can parse a single struct and switch on event. The fields that don't apply to an event are null rather than absent.

FieldTypeDescription
eventstring"job.completed" or "job.failed".
job_idstringThe job ID returned by POST /api/clips.
clip_idstring, nullableID of the rendered clip. Null on job.failed.
clip_urlstring, nullablePublic URL to an embeddable player page for the rendered clip — drop it straight into an <iframe>. This is an HTML page, not a media file. Null on job.failed.
video_urlstring, nullablePublic URL to the rendered mp4 itself. Use this wherever a media URL is expected — a <video> tag, a download, or an og:video tag (see Embedding Clips). Null on job.failed.
thumbnail_urlstring, nullablePublic URL to a poster JPG. Null on job.failed.
clip_durationnumber, nullableLength of the rendered clip in seconds, as a float. Null on job.failed. 0 in the rare case the clip rendered but we couldn't read its duration — the clip itself is still fine, so don't treat 0 as a failure.
statusstring"processed" on job.completed, "error" on job.failed.
error_codestring, nullableWhy the job failed — see Error codes. Null on job.completed.
extrasany JSON value, nullableEchoed back exactly as sent in the request's extras field (see Submitting Render Jobs), or null if you didn't send one. Round-tripped unmodified — object/array/number/string/boolean values keep their original JSON type, nothing is stringified.

Example: job.completed

{
"event": "job.completed",
"job_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"clip_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"clip_url": "https://dinkem.gg/clips/3fa85f64-5717-4562-b3fc-2c963f66afa6/embed",
"video_url": "https://hel1.your-objectstorage.com/dnkm/3fa85f64-5717-4562-b3fc-2c963f66afa6.mp4",
"thumbnail_url": "https://hel1.your-objectstorage.com/dnkm/3fa85f64-5717-4562-b3fc-2c963f66afa6.jpg",
"clip_duration": 24.5,
"status": "processed",
"error_code": null,
"extras": {
"gameId": "0408314d-53fa-4fa6-8770-c890171319b8",
"steam64Id": "76561198012345678",
"roundNumber": 14
}
}

Example: job.failed

{
"event": "job.failed",
"job_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"clip_id": null,
"clip_url": null,
"video_url": null,
"thumbnail_url": null,
"clip_duration": null,
"status": "error",
"error_code": "demo_incompatible",
"extras": {
"gameId": "0408314d-53fa-4fa6-8770-c890171319b8",
"steam64Id": "76561198012345678",
"roundNumber": 14
}
}

Error codes

error_code accompanies every job.failed and names what went wrong.

CodeMeaning
demo_url_rejectedThe demo_url isn't on a domain we accept. Nothing to retry — fix the URL.
demo_404The demo URL returned 404. Nothing to retry — the file is missing or expired.
demo_incompatibleWe couldn't play the demo with the current CS2 build.
player_not_in_demoThe steam64_id you submitted isn't one of the players in this demo. Nothing to retry — check the id is right for this demo, or that you sent the right demo.
no_kills_foundThe player has no kills in the round(s) you requested, so there's nothing to render. Nothing to retry — check the round number, or that the player got a kill in it.
workshop_not_supportedThe demo was recorded on a workshop map that requires additional Steam Workshop content we don't currently support. Nothing to retry — the same demo will fail again.
demo_failed_parseWe couldn't parse the demo file — it looks incomplete or corrupted (the recording appears cut off). Nothing to retry — the same file will fail again.
decompress_failedWe couldn't decompress the demo archive — the download looks truncated or corrupted (a checksum mismatch). Nothing to retry — the same file will fail again.
download_timeoutWe couldn't download the demo fast enough, across every retry. Usually the host serving it.
download_errorThe demo download failed at the network level, across every retry.
cs2_unavailableA render machine crashed or stalled mid-render. The demo is fine — we retry it automatically on another machine, so this only reaches you if every retry hit the same trouble.
render_failedThe render itself failed.
upload_errorThe clip rendered, but uploading it to our storage failed.
finalize_failedThe clip rendered, but we couldn't finish processing it.

This list is open-ended. New codes get added as we learn to name failures more precisely, and that is not a breaking change — so don't exhaustively switch on it. Treat any code you don't recognize as a generic failure.

download_timeout, download_error, and cs2_unavailable reaching you at all means we already retried the job repeatedly and exhausted it; there's no partial state to reconcile.

Status values

status is a coarser signal than error_code, for integrations that only need "did it work":

  • processed — always paired with job.completed.
  • error — always paired with job.failed.
  • submitted — represented by the synchronous POST /api/clips response, not a webhook.
  • ondemand — not a concept in dinkem.gg; this value never applies here.