---
title: "Webhook: job.completed"
method: POST
path: "job.completed"
tags: ["Webhooks"]
---

# Webhook: job.completed

`POST job.completed` (webhook)

POSTed to the job's registered webhook URL when the job completes. Delivery is at-least-once: dedupe on `X-Hedra-Webhook-Id` and verify `X-Hedra-Webhook-Signature` before acting.

## Headers

- `X-Hedra-Webhook-Id` string, required
- `X-Hedra-Webhook-Timestamp` string, required
- `X-Hedra-Webhook-Event` 'job.completed', required
- `X-Hedra-Webhook-Attempt` integer, required
- `X-Hedra-Webhook-Redelivery` 'true' | 'false', required
- `X-Hedra-Webhook-Signature` string, required

## Payload

- WebhookPayload: Body of a webhook delivery: the `GET /jobs/{job_id}` result envelope without its poll-only fields (`logs`, `cost`, `currency`) — poll the job to read those.
  - `job_id` string, required: The job this envelope describes.
  - `model` string, required: The resolved model id this job ran on.
  - `quality` string, nullable: The quality level this job ran at; present only for models that offer quality levels.
  - `status` 'IN_QUEUE' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED', required: The four states a job can be in.
  - `prompt` string, nullable: The prompt this job ran with. When `enhance_prompt` was set, this is the rewritten prompt the model received rather than the one submitted. Absent on models that take no prompt.
  - `outputs` OutputItem[]: The job's outputs — always an array, even for a single output; empty until the job completes. Always empty for a chat completion, whose text the chat response returned.
    - `status` 'COMPLETED' | 'FAILED' | 'EXPIRED': The state of a single output within a job's result.
    - `asset_id` string, nullable: This output's asset id — server-issued, and opaque. Pass it as `{"source": "asset", "asset_id": ...}` in a later submit's media inputs to reuse this output as a reference. Null once the output has expired, since its bytes are no longer retrievable.
    - `voice_id` string, nullable: The voice a voice-clone job created (`voice_<uuid>`). Send it back as `voice_id` on a text-to-speech submit to speak with this voice; it stays usable by the account that created it. Null for every other kind of output.
    - `url` string, uri, nullable: Presigned download URL for the output bytes, or null once the output has expired. Each call to `GET /v3/jobs/{job_id}` returns a newly signed one.
    - `expires_at` string, date-time, nullable: Download or reuse this output before this ISO-8601 instant. After it, `status` can change to `EXPIRED`, with `url`, `asset_id` and `expires_at` null; `DELETE /v3/jobs/{job_id}` makes that change at once. Null for an output that does not expire, such as a cloned voice.
    - `content_type` string, nullable: MIME type of the output bytes.
    - `width` integer, nullable: Width in pixels; null for an output with no frame (audio).
    - `height` integer, nullable: Height in pixels; null for an output with no frame (audio).
    - `duration_ms` integer, nullable: Duration in milliseconds; null for a still image.
    - `fps` integer, nullable: The video's measured frame rate; null for a non-video output and for a video that has not been probed yet.
    - `error` ErrorEnvelope
      - `code` 'UNKNOWN' | 'INVALID_ARGUMENT' | 'NOT_FOUND' | 'GONE' | 'ALREADY_EXISTS' | 'ALREADY_IN_PROGRESS' | 'UNAUTHORIZED' | 'PERMISSION_DENIED' | 'INSUFFICIENT_BALANCE' | 'MODERATION_FAILED' | 'FAILED_PRECONDITION' | 'CANCELLED' | 'DEADLINE_EXCEEDED' | 'RESOURCE_EXHAUSTED' | 'UNAVAILABLE' | 'INTERNAL', required: Universal error code space. Modeled after gRPC status codes. Every exception in the system carries an ErrorCode. SDK clients map 3rd-party errors to these codes at the lowest level. Retry logic, HTTP status mapping, and OTEL metrics all key off this enum. NOTE: The semantic meaning of these error codes is roughly mapped from the semantic of the gRPC status codes: see https://grpc.io/docs/guides/status-codes/ for details.
      - `message` string, required: Human-readable summary of the error. Fixed per condition — match on `code`, not on this text.
      - `retryable` boolean: Whether retrying the same request can succeed. Describes the condition, not a promise — pair with `retry_after` when present.
      - `retry_after` integer, nullable: Seconds to wait before retrying; set when the error is retryable. Mirrors the `Retry-After` response header.
      - `param` string, nullable: The primary offending input field, when the error is about one specific field.
      - `details` FieldError[], nullable: Every field-level problem, when the error is a validation failure — all of them at once, not just the first.
        - `field` string, required: Dotted path to the offending input (e.g. `input.resolution`).
        - `message` string, required: What is wrong with this field's value.
        - `reason` string, nullable: Machine-readable hint for which constraint failed ("required", "enum", "type", …).
        - `allowed` string[], nullable: Accepted values for an enum, or accepted field names on the first unknown-field violation. Omitted on subsequent unknown fields.
      - `replaced_by` string, nullable: The id of a successor model, set when the requested model has been retired (code `GONE`) and replaced; null otherwise. Lets a client programmatically migrate off a retired model.
      - `billing` BillingError: Why a request was refused for funds, and what would clear it. The envelope's structured detail for `INSUFFICIENT_BALANCE`, the way `details` is its structured detail for a validation failure. Every amount is in `currency`, and `balance` is the same number `GET /balance` returns — so a client can decide whether to top up, wait, or fail over without a second round trip to work out which balance was short.
        - `balance` number, required: Spendable balance on the account this request bills, at the moment it was refused. The same value `GET /balance` returns.
        - `required` number, nullable: What this request needed. Null when the refusal is a funding precondition rather than a priced one — an upload is not priced, and some models cannot be quoted until their inputs are measured (`POST /models/{model}/estimate` says so explicitly).
        - `currency` string: ISO-4217 code the amounts above are denominated in.
        - `funding_url` string, required: Where a human can add funds to the account this request bills. The API itself cannot add them.
        - `reason` string, nullable: Why the automatic top-up did not fund the balance, when known. `card_declined`: the card that pays for automatic top-ups was declined, and the payment method must be updated on the billing page before a top-up can succeed. Clients MUST tolerate unrecognized values: new reasons may be added at any time.
        - `affordable_output_tokens` integer, nullable: On a chat request's refusal, the largest output bound in tokens, `max_tokens` or `max_output_tokens` on `POST /responses`, whose hold the balance covers for this request's prompt. A chat request (`POST /chat/completions`, `POST /responses`, `POST /anthropic/v1/messages`) holds its worst-case price, `required`, until it completes. The balance covers the hold of the same request with this bound until the balance changes. 0 when the balance does not cover the prompt with the model's smallest output bound. Null on every other refusal.
      - `type` 'invalid_request_error' | 'authentication_error' | 'permission_error' | 'rate_limit_error' | 'server_error', required: The OpenAI error type for an error's HTTP status, published so an OpenAI SDK's `APIStatusError.type` carries a value.
  - `metrics` Metrics: Timing measured for a completed job.
    - `processing_time_ms` integer, nullable: Wall-clock milliseconds between this job's `started` and `completed` lifecycle events. It brackets provider queueing, generation, output download, and any failed attempts with their retry backoff, so it measures the whole job rather than the model's own inference time, and it is not a provider-reported figure. Polled results and webhook deliveries report the same value. Null when the job did not record both events, after `DELETE /v3/jobs/{job_id}`, and possibly once the job's output is `EXPIRED`. For a chat completion, the milliseconds from dispatching the request to the provider until its last byte.
    - `time_to_first_token_ms` integer, nullable: For a chat completion, the milliseconds from dispatching the request to the provider until its first chunk. Absent for other jobs.
  - `usage` ChatUsage: The tokens a chat completion used, as the model reported them.
    - `prompt_tokens` integer, required: Tokens in the prompt.
    - `cached_tokens` integer, nullable: Of `prompt_tokens`, how many the model served from cache, which Hedra charges at the model's cached input rate; null when the model did not report the count.
    - `completion_tokens` integer, required: Tokens generated, reasoning tokens included.
    - `reasoning_tokens` integer, nullable: Of `completion_tokens`, how many the model spent reasoning; null when the model reports none.
    - `estimated` boolean, required: True when the model reported no usage and the counts are estimated from the request and response bytes.
  - `error` ErrorEnvelope
    - `code` 'UNKNOWN' | 'INVALID_ARGUMENT' | 'NOT_FOUND' | 'GONE' | 'ALREADY_EXISTS' | 'ALREADY_IN_PROGRESS' | 'UNAUTHORIZED' | 'PERMISSION_DENIED' | 'INSUFFICIENT_BALANCE' | 'MODERATION_FAILED' | 'FAILED_PRECONDITION' | 'CANCELLED' | 'DEADLINE_EXCEEDED' | 'RESOURCE_EXHAUSTED' | 'UNAVAILABLE' | 'INTERNAL', required: Universal error code space. Modeled after gRPC status codes. Every exception in the system carries an ErrorCode. SDK clients map 3rd-party errors to these codes at the lowest level. Retry logic, HTTP status mapping, and OTEL metrics all key off this enum. NOTE: The semantic meaning of these error codes is roughly mapped from the semantic of the gRPC status codes: see https://grpc.io/docs/guides/status-codes/ for details.
    - `message` string, required: Human-readable summary of the error. Fixed per condition — match on `code`, not on this text.
    - `retryable` boolean: Whether retrying the same request can succeed. Describes the condition, not a promise — pair with `retry_after` when present.
    - `retry_after` integer, nullable: Seconds to wait before retrying; set when the error is retryable. Mirrors the `Retry-After` response header.
    - `param` string, nullable: The primary offending input field, when the error is about one specific field.
    - `details` FieldError[], nullable: Every field-level problem, when the error is a validation failure — all of them at once, not just the first.
      - `field` string, required: Dotted path to the offending input (e.g. `input.resolution`).
      - `message` string, required: What is wrong with this field's value.
      - `reason` string, nullable: Machine-readable hint for which constraint failed ("required", "enum", "type", …).
      - `allowed` string[], nullable: Accepted values for an enum, or accepted field names on the first unknown-field violation. Omitted on subsequent unknown fields.
    - `replaced_by` string, nullable: The id of a successor model, set when the requested model has been retired (code `GONE`) and replaced; null otherwise. Lets a client programmatically migrate off a retired model.
    - `billing` BillingError: Why a request was refused for funds, and what would clear it. The envelope's structured detail for `INSUFFICIENT_BALANCE`, the way `details` is its structured detail for a validation failure. Every amount is in `currency`, and `balance` is the same number `GET /balance` returns — so a client can decide whether to top up, wait, or fail over without a second round trip to work out which balance was short.
      - `balance` number, required: Spendable balance on the account this request bills, at the moment it was refused. The same value `GET /balance` returns.
      - `required` number, nullable: What this request needed. Null when the refusal is a funding precondition rather than a priced one — an upload is not priced, and some models cannot be quoted until their inputs are measured (`POST /models/{model}/estimate` says so explicitly).
      - `currency` string: ISO-4217 code the amounts above are denominated in.
      - `funding_url` string, required: Where a human can add funds to the account this request bills. The API itself cannot add them.
      - `reason` string, nullable: Why the automatic top-up did not fund the balance, when known. `card_declined`: the card that pays for automatic top-ups was declined, and the payment method must be updated on the billing page before a top-up can succeed. Clients MUST tolerate unrecognized values: new reasons may be added at any time.
      - `affordable_output_tokens` integer, nullable: On a chat request's refusal, the largest output bound in tokens, `max_tokens` or `max_output_tokens` on `POST /responses`, whose hold the balance covers for this request's prompt. A chat request (`POST /chat/completions`, `POST /responses`, `POST /anthropic/v1/messages`) holds its worst-case price, `required`, until it completes. The balance covers the hold of the same request with this bound until the balance changes. 0 when the balance does not cover the prompt with the model's smallest output bound. Null on every other refusal.
    - `type` 'invalid_request_error' | 'authentication_error' | 'permission_error' | 'rate_limit_error' | 'server_error', required: The OpenAI error type for an error's HTTP status, published so an OpenAI SDK's `APIStatusError.type` carries a value.

## Acknowledgement `2XX`

Acknowledged; any `2xx` stops the retry ladder. Other responses are retried per the Webhooks tag.

---

[API](https://skmtc.dev/hedra-labs/apis/hedra-api.md) - [All operations](https://skmtc.dev/hedra-labs/apis/hedra-api/llms.txt) - [OpenAPI document](https://skmtc.dev/hedra-labs/apis/hedra-api/revisions/6ef35a11cefe?raw)
