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
2xxresponse 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 firstpendingrefund update 12 hours after the job completes. Retries don't extend that time. - Duplicates and order. Deduplicate deliveries by the
X-SiC-Event-IDheader. 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 ashttps://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 type | Sent when | Body |
|---|---|---|
x402.outcome | A 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.payment | A 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"
}
}
| Field | Description |
|---|---|
event_id | An 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_at | The UTC time of the event. It never changes, so use it to detect a stale update that arrives out of order. |
job_id | The job's ID. It's omitted when the purchase ended before a job existed. |
paid_cents | What you paid. |
retained_cents | What the service kept. It's 0 for a full refund. |
refund.amount_cents | The refund amount. |
refund.status | pending, 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.