# Troubleshoot HTTP delivery

Source: https://stormedo.com/docs/how-to/troubleshoot-delivery/

Diagnose unknown acceptance, missing history, failed attempts, signature mismatches, rate limits, and recurring schedule problems.

Start with the API call’s HTTP status and, if available, the durable `data.id`. The top-level `request_id` identifies the API call; `data.id` identifies the outbound request. Keep both when investigating.

## Submission or history problems

| Symptom                                            | Meaning and next action                                                                                                                                                              |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| API `202`, destination not called yet              | Work is accepted. Inspect its status, `scheduled_at`, and latest attempt’s `retry.next_attempt_at`. `delivering` includes retry waits.                                               |
| Timeout, connection lost, or API `500`/`502`/`503` | Acceptance may be unknown. Retry unchanged input with the original idempotency key within 24 hours. Without a key, inspect before resubmitting because work may already exist.       |
| API `401`                                          | Check the server-side project key and bearer header. A revoked key must be replaced. Do not send this key to the receiver.                                                           |
| API `409`                                          | Compare the original idempotency input, or inspect lifecycle eligibility for cancel/replay. Do not silently switch to a new key.                                                     |
| API `413` or `422`                                 | Check size limits and `error.details`. Correct the input before retrying. The destination must be public HTTPS.                                                                      |
| API `429` with `plan_limit_exceeded`               | Workspace allowance is exhausted. Inspect `error.details`, upgrade, delete an unused schedule if applicable, or wait until `resets_at`. Tight-loop retries do not restore allowance. |
| An initial read returns `404` after `202`          | Allow the query projection a short time to catch up, then retry the read with a bounded wait. Confirm the ID and project. Do not recreate the accepted request.                      |
| Persistent read `404`                              | Check for a typo, another project’s key, or terminal history outside the plan’s visibility window. Active work remains visible.                                                      |

For example, retry a new request’s read every second for up to 30 seconds, then stop and surface the missing-history result for investigation. This is a bounded diagnostic window, not a promised projection deadline. A scheduled request can legitimately remain active much longer.

## Destination failures

Inspect `response_status_code`, `error_message`, `attempt_number`, and `retry` on completed attempts.

| Attempt outcome                                     | Next action                                                                                                                                                     |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `2xx` but business work did not finish              | Stormedo treats every `2xx` as success and does not inspect response bodies. Acknowledge only after durable processing or durable handoff.                      |
| `3xx`                                               | Redirects are not followed. Submit the final public HTTPS URL.                                                                                                  |
| `400`, `401`, `403`, `404`, or other ordinary `4xx` | Fix payload, destination authentication, route, or permissions. These are terminal unless the status is `408` or `429`. Replay only after correcting the cause. |
| `408`, `429`, or `5xx`                              | Retryable while attempts remain. Inspect the recorded next attempt time. Destination `Retry-After` can extend the wait up to 24 hours.                          |
| DNS, connection, or timeout error                   | Verify public DNS, reachability, TLS, and receiver latency. The receiver may have processed a timed-out attempt, so keep duplicate handling in place.           |
| No retry despite a temporary failure                | Check total attempt budget. Quick send, raw SDK sends, and schedules default to one attempt; advanced JSON requests default to three.                           |
| Cancel returned `202` but request succeeded         | The cancellation signal raced with an in-flight attempt. Cancellation cannot undo a completed business effect.                                                  |
| Replay returns `409`                                | The original is still active. Wait for `succeeded`, `failed`, or `cancelled`, then replay if appropriate.                                                       |

## Verification failures

Reject unverified deliveries before processing. Log the verifier error code and your operation’s outcome, without logging API keys, destination secrets, or the complete signature token.

| Verifier code                                              | Check                                                                                                                  |
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `missing_header`, `duplicated_header`, `malformed_header`  | Require exactly one `Stormedo-Signature` and one `Stormedo-Request-Id`. Inspect proxy header handling.                 |
| `body_mismatch`                                            | Capture original bytes before JSON parsing, character decoding, decompression, or re-serialization.                    |
| `audience_mismatch`                                        | Use the submitted public URL, including path, query, and trailing slash. Do not use the receiver’s internal proxy URL. |
| `project_mismatch` or `issuer_mismatch`                    | Configure the expected project and production issuer. Do not derive trusted values from token claims.                  |
| `method_mismatch` or `request_id_mismatch`                 | Pass the received method and single request-ID header. Check middleware rewrites.                                      |
| `token_expired` or `token_not_yet_valid`                   | Synchronize server clocks and verify immediately at ingress. Do not disable time checks.                               |
| `jwks_unavailable`                                         | Public-key retrieval failed. Return a temporary failure such as `503`; never bypass verification.                      |
| `unknown_signing_key`, `invalid_jwks`, `invalid_signature` | Confirm the trusted JWKS endpoint and key rotation/cache behavior. Reject the request and investigate.                 |

Use the [complete receiver examples](https://stormedo.com/docs/how-to/receive-deliveries/) to compare middleware order and error handling.

## Recurring schedule problems

A schedule’s `enqueued` outcome confirms creation of an occurrence, not completed delivery. Inspect requests with `source=recurring` and the matching `schedule_id`.

`skipped_usage_limit` means no request was created for that occurrence. Restore allowance for future runs. A paused schedule has no `next_run_at`; resume it to schedule the next future occurrence. Neither outages nor pauses create a catch-up burst. See [Schedule recurring requests](https://stormedo.com/docs/how-to/schedule-requests/).
