delivery_already_succeeded

The endpoint already accepted this delivery. Replaying it would send a duplicate; pass `force` if you need it anyway.

HTTP 409The resource is not in a state that allows this operation.

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

  1. Check whether you meant a different delivery. `GET /v1/webhooks/{id}/deliveries?status=failed` lists only the ones that never landed.
  2. 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.
  3. 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}`.
  4. 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.
  5. 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.
  • force is 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 force exists 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.

Hit an error that isn't covered? Email hello@repull.dev with the request id from the response headers.

AI