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

# Troubleshooting

> Nothing arriving, signatures failing, or the same event arriving twice.

Start at the delivery log. Open the endpoint under **Settings → Developers → Webhooks**: it records every delivery before anything is sent, so the difference
between *Custral never tried* and *your server refused* is visible there.

| What the log shows                | What it means                                                                                                                |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| No row at all                     | The event did not match this subscription, or it never emitted                                                               |
| `pending`                         | Queued, not yet attempted                                                                                                    |
| `failed` with a `responseStatus`  | Your endpoint answered, and not with a `2xx`. A `3xx` is a redirect Custral did not follow.                                  |
| `failed` with no `responseStatus` | Custral could not reach you, or refused to: DNS, TLS, connection refused, the 10 second timeout, or an `unsafe_url:` refusal |
| `success`                         | Your endpoint returned `2xx`. If nothing happened on your side, the problem is after the response.                           |

The `error` column says which. Every code is listed under
[Error codes](/dev/webhooks/delivery#error-codes).

## Nothing is arriving

<AccordionGroup>
  <Accordion title="The event is not one Custral emits">
    The [catalog](/dev/webhooks/events/overview) is the complete list of the
    nineteen events Custral delivers. An event that is not on it never fires, so
    there is nothing to wait for.
  </Accordion>

  <Accordion title="The change came from a bulk write">
    Imports, job runs and AI property fills are **suppressed** so one import does
    not become ten thousand deliveries. The record really did change and no
    webhook was sent. See
    [Bulk writes](/dev/webhooks/events/overview#bulk-writes-do-not-fire-webhooks).
  </Accordion>

  <Accordion title="The subscription does not match">
    Matching is exact, `prefix.*`, or `*`. `record.update` does not match
    `record.updated`, and `records.*` does not match anything. Read the event
    list back from the endpoint's row rather than from memory.
  </Accordion>

  <Accordion title="The endpoint is inactive, or was deleted">
    An inactive endpoint records its delivery as failed with
    `webhook_inactive_or_missing` and does not retry.
  </Accordion>

  <Accordion title="Custral refused the URL">
    The log shows `unsafe_url:` and a reason. The URL is re-checked on every
    attempt, not only when it was created, so a URL edited to something internal
    stops working immediately. The reasons are listed under
    [What Custral will and will not call](/dev/webhooks/security#what-custral-will-and-will-not-call).

    `unsafe_url:resolves_to_private_ip` means the hostname looks public but its
    DNS answers with a private address, such as an internal load balancer or a
    record left over from a staging setup. Every address the hostname resolves
    to has to be public, so one private answer among several is enough to
    refuse it.
  </Accordion>

  <Accordion title="Your endpoint redirects">
    The log shows `redirect_not_followed`, with the `3xx` in `responseStatus`.
    Custral does not follow redirects, so the delivery never reaches the address
    your server redirects to. The common causes are an `http://` URL on a server
    that forces `https://`, a trailing slash your framework adds or strips, and
    an apex domain that redirects to `www`.

    Register the URL your endpoint redirects to. A redirect is retried like any
    other failure, so a deploy that briefly answers with one recovers on its
    own. See [Redirects are not followed](/dev/webhooks/delivery#redirects-are-not-followed).
  </Accordion>

  <Accordion title="Your endpoint was down">
    Deliveries that failed during the outage are retried on the
    [retry schedule](/dev/webhooks/delivery#the-retry-schedule):
    **up to 8 attempts over about 24 hours**, with waits that grow from 5
    seconds to 18 hours. So an event can arrive hours after your endpoint is
    back online. A `failed` row with `attempts` below 8 is still waiting for
    its next try, unless its `error` is `webhook_inactive_or_missing` or starts
    with `unsafe_url:`.

    If all 8 attempts fail, the delivery is not tried again and there is no way
    to replay it by hand. Reconcile from the [API](/dev/api-reference/records)
    after an outage that lasted longer than about a day.
  </Accordion>
</AccordionGroup>

## Signature verification keeps failing

<AccordionGroup>
  <Accordion title="Your framework parsed the body">
    The most common cause by a distance. Verification needs the exact bytes that
    were signed, and re-serializing a parsed object does not reproduce them.
    Mount a raw body parser on the webhook route only:

    ```ts theme={null}
    app.post("/hooks/custral", express.raw({type: "application/json"}), custral.webhooks.express());
    ```

    The SDK throws `invalid_payload` with this exact advice when it is handed an
    object.
  </Accordion>

  <Accordion title="The secret is from a different endpoint">
    Each endpoint has its own secret. Two endpoints pointing at one URL need the
    right secret per subscription, or verification fails for one of them.
  </Accordion>

  <Accordion title="The secret was rotated">
    Rotation is immediate and has no overlap window. Deliveries signed with the
    new secret fail against the old one from the next event onward.
  </Accordion>

  <Accordion title="Your clock is off">
    The default tolerance is 300 seconds either side. A container with a drifted
    clock rejects perfectly good deliveries. Compare your host's time against
    the `t` value in the header.
  </Accordion>

  <Accordion title="You are comparing with ===">
    Fix the timing leak while you are there, but note this does not usually
    cause a false negative. If a constant-time compare fails, the bytes really
    do differ.
  </Accordion>
</AccordionGroup>

## The same event arrives twice

Expected, and your handler has to tolerate it. The usual cause is a timeout:
your server did the work, the response took more than 10 seconds, Custral never
saw it and retried.

Deduplicate on the envelope's `id`, which is stable across retries of one
delivery. `createdAt` is not: it is stamped per attempt. See
[Your handler must be idempotent](/dev/webhooks/delivery#your-handler-must-be-idempotent).

## Events arrive out of order

Also expected. Deliveries are processed concurrently. If order matters, treat
the payload as a nudge and read current state back from the API.

## A value looks like gibberish

`["opt_3Kd8sM"]` is a select's stored value, not a bug. Select, status, relation
and user fields carry ids. Resolve them from the object's schema, as shown in
[Payload format](/dev/webhooks/payload#field-values-are-stored-values-not-display-values).

## Still stuck

Quote the delivery id (`whd_…`) and the endpoint id (`hook_…`) when you write
in. Both are on the delivery log row, and together they identify one specific
attempt end to end.

<Card title="Get support" icon="life-ring" href="/support">
  Or email [hello@custral.com](mailto:hello@custral.com).
</Card>


## Related topics

- [Troubleshooting](/comms/sms/troubleshooting.md)
