delivery_already_succeeded
The endpoint already accepted this delivery. Replaying it would send a duplicate; pass `force` if you need it anyway.
When it fires
You called POST /v1/webhooks/{id}/deliveries/{delivery_id}/replay for a delivery your endpoint already accepted. It returned a 2xx, so the event was handed over successfully — replaying it would only deliver a duplicate of something you have already processed.
Repull refuses by default rather than quietly re-sending, because a duplicate is the more expensive mistake: it can double-charge, double-notify, or double-write on the receiving side.
Most often this means you replayed the wrong id. A failed delivery and its successful automatic retry are separate rows in GET /v1/webhooks/{id}/deliveries, and it is easy to grab the one that already went through.
Response shape
Every Repull error follows the same envelope. The code is stable and safe to switch on.
{
"error": {
"code": "delivery_already_succeeded",
"message": "Delivery del_xyz789 was accepted by your endpoint (HTTP 200). Replaying it would send a duplicate event.",
"fix": "If you need it re-sent anyway, repeat the request with body {\"force\": true}. Dedupe on the X-Repull-Event-Id header first — a forced replay is a duplicate by design.",
"docs_url": "https://repull.dev/docs/errors/delivery_already_succeeded",
"request_id": "req_01J5X7Y8Z9ABCDEF12345678"
}
}How to fix
- Check whether you meant a different delivery. `GET /v1/webhooks/{id}/deliveries?status=failed` lists only the ones that never landed.
- If the event really did reach you, there is nothing to replay — the automatic retries already succeeded. Look for the event in your own logs by its `eventId`, not by the delivery id.
- If you genuinely need it re-sent — you dropped it on your side, or you are rebuilding state — repeat the call with body `{"force": true}`.
- Make sure your handler is idempotent before forcing. Dedupe on the `X-Repull-Event-Id` header, which is stable across replays; the delivery id changes every time.
- Remember a forced replay still counts against the 3-per-hour budget for that delivery.
Common gotchas
- Do not retry the same call unchanged. This is not transient — the delivery will still have succeeded a second later. Either pick a different delivery or add
force. forceis not a limit override. It overrides this one refusal only. A forced replay is counted like any other, and a delivery that has already used its 3 for the hour returns replay_limit_reached whether or not you pass it.- Success means 2xx, not “handled”. If your endpoint acknowledges before processing and then throws in a background job, Repull recorded a success. That is the case
forceexists for. - The replay you create can fail. Forcing a re-send of a previously successful delivery does not guarantee a 2xx this time; it is a fresh attempt against your endpoint as it is right now.
Examples
curl
# Replaying a delivery that already landed
curl -X POST https://api.repull.dev/v1/webhooks/wh_abc123/deliveries/del_xyz789/replay \
-H "Authorization: Bearer sk_live_YOUR_KEY"
# HTTP/1.1 409 Conflict
# { "error": { "code": "delivery_already_succeeded", ... } }
# Did you mean one of the failures?
curl "https://api.repull.dev/v1/webhooks/wh_abc123/deliveries?status=failed" \
-H "Authorization: Bearer sk_live_YOUR_KEY"
# Send it anyway — counts against the 3-per-hour replay budget
curl -X POST https://api.repull.dev/v1/webhooks/wh_abc123/deliveries/del_xyz789/replay \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "force": true }'
# HTTP/1.1 200 OK
# { "id": "del_new456", "replayNumber": 1, "replaysRemaining": 2, "nextWindowResetAt": "..." }TypeScript
const API = 'https://api.repull.dev'
const headers = {
Authorization: `Bearer ${process.env.REPULL_API_KEY}`,
'Content-Type': 'application/json',
}
/**
* Replay a delivery. Pass force only when you know the event was lost on your
* side — it deliberately sends a duplicate, so the handler must dedupe on the
* X-Repull-Event-Id header.
*/
async function replay(webhookId: string, deliveryId: string, force = false) {
const res = await fetch(
`${API}/v1/webhooks/${webhookId}/deliveries/${deliveryId}/replay`,
{ method: 'POST', headers, body: JSON.stringify({ force }) },
)
if (res.status === 409) {
const { error } = await res.json()
if (error.code === 'delivery_already_succeeded') {
// Already processed. Retrying unchanged will fail identically.
return { skipped: true as const, reason: error.message }
}
if (error.code === 'replay_limit_reached') {
// force does not lift this one — the budget is spent for the hour.
throw new Error(`Replay budget spent until ${error.next_replay_allowed_at}`)
}
}
return res.json()
}If you're an AI agent
The delivery already reached the endpoint with a 2xx, so a replay would be a duplicate. Do NOT retry the call unchanged — it is not transient. First check you did not mean a different delivery: GET /v1/webhooks/{id}/deliveries?status=failed lists the ones that never landed. Only re-send with body {"force": true} if the user confirms the event was lost on their side and their handler dedupes on the X-Repull-Event-Id header, since a forced replay is a duplicate by design and still spends one of the 3 replays allowed per hour for that delivery.
Related
- Error reference — the full table of error codes
- Using Repull from AI agents — patterns for handling errors in agent loops
- Webhook retries & replay
- replay_limit_reached (409)
- Verify webhook signatures
- Idempotency keys
Hit an error that isn't covered? Email hello@repull.dev with the request id from the response headers.