> ## Documentation Index
> Fetch the complete documentation index at: https://www.hedra.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook: billingtop off failed

> POSTed to the default webhook URL when Stripe declines the payment method for an automatic top-up of the API wallet. After a decline, Hedra holds further automatic top-ups until the hold expires, the payment method changes or a top-up is paid, and sends no event for a decline while a hold is in place. Retried on the same schedule as a job delivery, but not listed by `GET /webhooks/deliveries` and cannot be redelivered. Dedupe on `X-Hedra-Webhook-Id` and verify `X-Hedra-Webhook-Signature` before acting.



## OpenAPI

````yaml /openapi-v3.json webhook billing.top_off_failed
openapi: 3.1.0
info:
  title: Hedra API v3
  description: >-
    Generate images, video, and audio with over 100 models through one API,
    including Veo 3.1, Seedance 2.0, Kling V3, GPT Image 2, Nano Banana Pro, and
    ElevenLabs V3. Create an API key and fund your wallet, then every model runs
    through the same three requests: submit a job, poll it, download the output.
    Input fields have the same names across models, such as `prompt`,
    `aspect_ratio`, `resolution`, `duration_ms`, and `start_image`, so switching
    models means changing the model id and choosing values the new model's
    schema accepts. Browse the [model
    catalog](https://www.hedra.com/develop/models) to pick one.


    **Authenticate** every request with your API key — create one in the
    [developer console](https://www.hedra.com/develop/api-keys): `Authorization:
    Key <key_id>:<secret>`.


    **Fund the API wallet** before your first call. It is prepaid and held in US
    dollars, it is separate from your Hedra Studio balance, and a workspace
    starts with **$0.00** in it; nothing you buy or hold in Studio moves money
    into it. Until it is funded, every generation is refused with `402
    INSUFFICIENT_BALANCE`; uploading inputs with `POST /files` is free and works
    on an empty wallet, so you can stage a request and only meet the gate at
    submit. Add funds in the [developer
    console](https://www.hedra.com/develop/billing), and read the wallet any
    time with `GET /balance`.


    **Quickstart** — submit a job, poll it, then read the output URL:


    ```bash

    # 1. Submit — capture the job id from the 202 ack

    JOB_ID=$(curl -sS -X POST https://api.hedra.com/v3/models/gpt-image-2 \
      -H "Authorization: Key $HEDRA_KEY" \
      -H "Content-Type: application/json" \
      -d '{"input": {"prompt": "a space cat", "quality": "medium", "aspect_ratio": "1:1", "resolution": "1K"}}' | jq -r .job_id)

    # 2. Poll until status is COMPLETED (or FAILED)

    curl https://api.hedra.com/v3/jobs/$JOB_ID/status -H "Authorization: Key
    $HEDRA_KEY"


    # 3. Read the result — outputs[].url holds the generated file

    curl https://api.hedra.com/v3/jobs/$JOB_ID -H "Authorization: Key
    $HEDRA_KEY"

    ```


    **Follow a job without polling** — a video model can run for minutes, so the
    loop above suits fast image jobs and little else. Submit with a `webhook`
    URL and Hedra delivers the finished result there, ed25519-signed and
    retried; set `PUT /webhooks/default` once to cover every job that names no
    URL of its own, and see *Webhooks* for the signature scheme and the retry
    ladder. To watch one job live instead, open `GET /jobs/{job_id}/stream` —
    Server-Sent Events carrying every status transition and lifecycle event
    until the job reaches a terminal state.


    **Find a model** — `GET /models` lists every model with its `id` and human
    `name`; each operation in the *Run a model* section is titled with both, so
    a model you know by name ("GPT Image 2") maps straight to the id you submit
    to (`gpt-image-2`). One model's typed input schema: `GET
    /models/{id}/openapi.json`.


    Estimate cost before you run with `POST /models/{model}/estimate`. For
    volume pricing and custom limits, [talk to
    us](https://www.hedra.com/enterprise).


    The full machine-readable spec is at
    `https://api.hedra.com/v3/openapi.json`.
  version: 3.17.8
servers:
  - url: https://api.hedra.com/v3
security: []
tags:
  - name: Run a model
    description: >-
      One typed submit endpoint per model — titled `Run <name> (<id>)`, so the
      model you know by name is the operation you call. Every one returns the
      same `202` job envelope; track it under Jobs.


      `POST /models/{model}` is the same call with the id supplied at runtime
      and `input` untyped — use it for a model your client was generated before,
      or one you looked up in `GET /models`.
  - name: Models
    description: Discover models and estimate cost.
  - name: Jobs
    description: Poll, stream, and list submitted jobs.
  - name: Webhooks
    description: >-
      Configure, test, verify, and inspect outbound webhooks.


      **Delivery and retries.** Delivery is at-least-once. A `2xx` from your
      endpoint is success; anything else — including a redirect, which is never
      followed — is retried. Hedra makes up to **12 attempts over approximately
      6 hours**, backing off 10s, 30s, 90s, 4m30s, 13m30s, 40m30s, then hourly.
      An endpoint that is unreachable for the whole window is marked `FAILED`
      and is not retried again; replay it with `POST
      /webhooks/deliveries/{job_id}/redeliver`. Two failures are permanent and
      stop the ladder immediately: a URL that resolves to a blocked address
      range, and a redirect pointing at one.


      Because the retry window is bounded, respond quickly and do your own
      processing asynchronously — acknowledge with a `2xx` first.


      **Deduplication.** Key on **`X-Hedra-Webhook-Id`**. It identifies the
      *event* — it is the job's own id, and it is byte-identical across every
      retry and every replay. Do **not** hash the request body: each attempt
      re-signs the output URL, so the body legitimately differs between attempts
      of the same event and a body hash would treat every retry as new.


      Each POST also carries `X-Hedra-Webhook-Attempt` (1-based, informational)
      and **`X-Hedra-Webhook-Redelivery`** (`true` / `false`). Redelivery is the
      exception to deduplication: when it is `true`, an operator asked for this
      event to be sent again — process it even if you have already recorded that
      id, because that request is the whole reason it was sent.


      **Inspecting deliveries.** `GET /webhooks/deliveries` reports each
      delivery's cumulative `attempts`, its latest outcome (`status`,
      `last_response_status`, `last_error`), and its replay history:
      `redelivery_count` counts operator replays, and `redeliveries` archives
      the outcome each replay superseded — a replay re-sends on the same
      delivery record and never erases the previous result. Each entry's
      `attempts` value is the cumulative total when that replay was requested,
      so the per-cycle attempt counts (and any total beyond the 12-attempt
      ladder) can be read off the record directly.


      **Why a delivery failed.** `last_error` — on the delivery and on every
      archived replay entry — is the same error envelope a failed job returns
      from `GET /jobs/{job_id}`, not a free-text diagnostic. Its `code` comes
      from the one error vocabulary this API uses, narrowed for delivery to this
      set:


      | `code` | Means |

      | --- | --- |

      | `DEADLINE_EXCEEDED` | Your endpoint did not respond before the delivery
      timeout. |

      | `UNAVAILABLE` | Your endpoint could not be reached (DNS, TLS, or
      connection failure) or answered `5xx` — or redirected, which is never
      followed. |

      | `RESOURCE_EXHAUSTED` | Your endpoint answered `429`. |

      | `UNAUTHORIZED` | Your endpoint answered `401`. |

      | `PERMISSION_DENIED` | Your endpoint answered `403`. |

      | `NOT_FOUND` | Your endpoint answered `404`. |

      | `INVALID_ARGUMENT` | Your endpoint answered some other `4xx`. |

      | `FAILED_PRECONDITION` | The URL is not a permitted webhook target — it
      resolves to a blocked address range, or redirects to one. Fix the URL;
      replaying will not help. |

      | `INTERNAL` | Hedra could not build the payload. |

      | `UNKNOWN` | No classification is available, including for deliveries
      that failed before this field became structured. |


      `retryable` describes the condition, not what Hedra did: every non-2xx
      response is retried on the ladder above, so it answers whether replaying
      the delivery is likely to help. The `message` is a fixed summary — your
      endpoint's URL, address, headers, credentials, and response body are never
      echoed back, so treat your own logs, not this field, as the record of what
      your endpoint returned.


      **Verifying a delivery.** Every request carries
      `X-Hedra-Webhook-Signature`, a hex-encoded ed25519 signature over a
      canonical string of **five newline-separated fields, in this order**:


      ```

      {X-Hedra-Webhook-Timestamp}

      {X-Hedra-Webhook-Id}

      {X-Hedra-Webhook-Event}

      {X-Hedra-Webhook-Redelivery}

      {sha256 hex digest of the raw request body}

      ```


      Rebuild that string from the request you received, verify it against the
      key from `GET /webhooks/public-key`, and reject the delivery if the
      timestamp is more than 5 minutes old. Hash the body **exactly as
      received**, before any JSON parsing or re-serialization.


      The signature deliberately covers the deduplication id and the redelivery
      flag, not just the body — both of them decide whether you process a
      duplicate, so an unsigned copy of either would let anyone who captured a
      delivery replay it past your idempotency check. Verify *before* you act on
      any header. `X-Hedra-Webhook-Attempt` is outside the signature and is
      informational only; do not branch on it.
  - name: Log drains
    description: >-
      Stream signed job lifecycle logs to HTTPS destinations.


      **Why the last batch failed.** `last_error` is the same error envelope a
      failed job returns from `GET /jobs/{job_id}`, not a free-text diagnostic.
      It is null until a batch fails and is cleared again on the next success.
      Its `code` comes from the one error vocabulary this API uses, narrowed for
      drain delivery to this set:


      | `code` | Means |

      | --- | --- |

      | `DEADLINE_EXCEEDED` | Your destination did not respond before the
      delivery timeout. |

      | `UNAVAILABLE` | Your destination could not be reached (DNS, TLS, or
      connection failure) or answered `5xx` — or redirected, which is never
      followed. |

      | `RESOURCE_EXHAUSTED` | Your destination answered `429`. |

      | `UNAUTHORIZED` | Your destination answered `401`. |

      | `PERMISSION_DENIED` | Your destination answered `403`. |

      | `NOT_FOUND` | Your destination answered `404`. |

      | `INVALID_ARGUMENT` | Your destination answered some other `4xx`. |

      | `FAILED_PRECONDITION` | The URL is not a permitted log drain target — it
      resolves to a blocked address range. Fix the URL; retrying will not help.
      |

      | `INTERNAL` | Hedra could not prepare the batch. |

      | `UNKNOWN` | No classification is available, including for drains that
      last failed before this field became structured. |


      `retryable` describes the condition, not what Hedra did: every failed
      batch is requeued until the drain auto-disables, so it answers whether
      fixing the destination and re-enabling is likely to help. The `message` is
      a fixed summary — your destination's URL, headers, credentials, and
      response body are never echoed back, so treat your own logs, not this
      field, as the record of what your destination returned.


      `disabled_reason` answers a different question and stays its own small
      vocabulary: `consecutive_failures` when five batches in a row failed,
      `disabled_by_user` when you turned the drain off.
  - name: Access
    description: API keys, browser tokens, and file uploads.
  - name: Billing
    description: Balance and usage, in US dollars.
paths: {}

````