Skip to main content

Webhooks

When you pay for a job with a webhook_url, the service sends a POST request to that URL when the job ends and whenever a refund changes status. Unlike Jobs API webhooks, x402 webhooks are retried and carry event headers.

Delivery​

  • Acknowledge with a 2xx within 10 seconds. Any 2xx response acknowledges the delivery. The service doesn't follow redirects, so a redirect counts as a failed delivery.
  • Retries. The service retries failed deliveries every five minutes at first, backing off to one hour. It makes at most 12 attempts within 24 hours of the event.
  • When it sends. A job's completion, failure, or cancellation is sent when the job becomes final. The outcome of a purchase that ends without a job, and every refund update, is sent within about five minutes.
  • Private jobs. For a job created with is_private, the service stops delivering the outcome and the first pending refund update 12 hours after the job completes. Retries don't extend that time.
  • Duplicates and order. Deduplicate deliveries by the X-SiC-Event-ID header. A retry keeps the same ID. Don't assume deliveries arrive in order.
  • No signature. Anyone who knows your webhook URL can send it requests. Treat a webhook as a notification and confirm the result by getting the job. You can add a secret to your webhook_url, such as https://your-domain.com/webhook?token=SOME_SECRET, and check it.

Webhooks for jobs created with an API key work differently. See Compared With x402 Webhooks.

Event Types​

The X-SiC-Event-Type header names the event:

Event typeSent whenBody
x402.outcomeA job completes, fails, or is canceled, or a purchase ends without a job.The result, as JSON, SRT, or VTT, or the failure details as JSON.
x402.paymentA refund is pending, succeeded, or failed.A refund update as JSON, even when output_format is srt or vtt.

Payloads​

Completed​

The body is the same JSON as a completed job, shortened here:

{
"id": "00000000-1111-4222-8333-555555555555-sic",
"output": {
"duration": 1246.679375,
"request": { "input_url": "https://example.com/audio-file.m4a" },
"segments": [
{
"confidence": 0.988,
"end": 7.296,
"id": 1,
"language": "en (99.60%)",
"processing_duration_in_s": 0.029,
"start": 0,
"text": "Imagine staring at um just a massive hard drive filled with gigabytes of raw video files.",
"words": []
}
]
},
"status": "COMPLETED"
}

If the job was created with an output_format, the body is the transcript in that format instead, with Content-Type: application/x-subrip for SRT or text/vtt for VTT. That body doesn't contain the job ID. To tell jobs apart, add your own identifier to each job's webhook_url, for example https://your-domain.com/webhook?ref=episode-42.

Failed​

{
"id": "00000000-1111-4222-8333-555555555555-sic",
"output": {
"error": "Expected at most 300 seconds; got at least 301 seconds",
"duration_error": {
"code": "INPUT_DURATION_MISMATCH",
"min_input_duration": 6,
"max_input_duration": 300,
"actual_input_duration": 301,
"actual_duration_is_lower_bound": true
}
},
"status": "FAILED"
}

output.error is a human-readable message that can change. duration_error appears only for a duration failure. See Get a Job for its fields. The service refunds a failed job in full.

Canceled​

{
"id": "00000000-1111-4222-8333-555555555555-sic",
"output": {
"error": "Transcription canceled"
},
"status": "CANCELED"
}

Ended Without a Job​

A purchase can end before a job exists, for example when the file URL is unreachable, an upload fails, or an upload purchase expires unused. The body has a purchase_id instead of an id, and status is FAILED or EXPIRED:

{
"purchase_id": "00000000-1111-4222-8333-444444444444",
"output": {
"error": "Input media could not be reached"
},
"status": "FAILED"
}

There's no job to get. Replay the paid request to read the purchase's status. The service refunds the purchase in full.

Refund Updates​

A refund update has X-SiC-Event-Type: x402.payment and its own JSON body. It's separate from the job's outcome, so a job can be COMPLETED while its refund is still pending:

{
"type": "x402.payment",
"event_id": "x402:00000000-1111-4222-8333-444444444444:refund:pending:0",
"occurred_at": "2026-09-30T12:06:00.000Z",
"purchase_id": "00000000-1111-4222-8333-444444444444",
"job_id": "00000000-1111-4222-8333-555555555555-sic",
"payment_status": "paid",
"paid_cents": 12,
"retained_cents": 3,
"refund": {
"id": "x402-refund:00000000-1111-4222-8333-444444444444",
"amount_cents": 9,
"status": "pending"
}
}
FieldDescription
event_idAn opaque ID for one status change. Deduplicate by it. A later change has a new ID, even if that status occurred before. A retry keeps the same ID.
occurred_atThe UTC time of the event. It never changes, so use it to detect a stale update that arrives out of order.
job_idThe job's ID. It's omitted when the purchase ended before a job existed.
paid_centsWhat you paid.
retained_centsWhat the service kept. It's 0 for a full refund.
refund.amount_centsThe refund amount.
refund.statuspending, succeeded, or failed. succeeded means the service issued the refund, not that your wallet already received it.

See Refunds for when the service refunds and how long a refund can take.