# Delivery, retries, deduplication, and ordering

Betterstood webhook events are delivered at least once. Treat the CloudEvent `id` as the durable idempotency key: verify the signature over the raw bytes, then atomically insert the id and accepted body or work item under a unique constraint before returning 2xx. A duplicate id must return 2xx without repeating business side effects.

See [Verify webhook signatures](/developers/webhooks/verify-signatures/) for tested TypeScript and Python receiver examples.

## Event modes, lifecycle, and ordering

Betterstood exposes exactly these three lifecycle subscriptions:

1. `com.elicitra.interaction.ended.v1` at the first canonical terminal transition, with `data.lifecycleSequence: 1`.
2. `com.elicitra.analysis.completed.v1` when extraction commits as `VALID` and the interaction reaches `ANALYSIS_READY`, with sequence `2`.
3. `com.elicitra.analysis.failed.v1` only after a terminal analysis failure and exhausted internal retries, also with sequence `2`. An `INVALID` extraction is not terminal.

Completed and failed analysis events are mutually exclusive for an interaction. Network delivery order is not guaranteed, so sequence is only a lifecycle hint. Store events independently and reconcile by interaction id and `lifecycleSequence`; do not wait indefinitely for sequence 1 before accepting sequence 2.

`com.elicitra.automation.triggered.v1` is the fourth public event and has delivery mode `conditional_destination`. It is not available in endpoint lifecycle subscriptions. Betterstood sends it only when a configured conditional rule matches and selects that endpoint as its destination. It omits `data.lifecycleSequence`; deduplicate it by event id and use `data.activation.executionId` to correlate the selected action.

Every event id remains stable across retries and manual replay and is globally unique within Betterstood's fixed CloudEvents `source`. For lifecycle events, Betterstood deterministically reserves one event for each organization, interaction, lifecycle slot, and schema version.

## Retry schedule

Attempts are scheduled at absolute offsets from the first attempt:

| Attempt |      Offset |
| ------: | ----------: |
|       1 | immediately |
|       2 |    1 minute |
|       3 |   5 minutes |
|       4 |  30 minutes |
|       5 |     2 hours |
|       6 |     6 hours |
|       7 |    24 hours |
|       8 |    72 hours |

Each non-zero offset has deterministic jitter of ±10%. A valid `Retry-After` selects the later of the normal schedule and the requested time, with the requested delay clamped between 1 minute and 24 hours.

- Any 2xx response succeeds.
- `410 Gone` disables the endpoint.
- Redirects are never followed.
- Every other non-2xx response or network error retries until the delivery becomes dead letter.
- Each request times out after 10 seconds. Betterstood reads at most 64 KiB of response data and never persists the response body.

## Replay and backfill

A manual replay creates another delivery of the same CloudEvent with the same id. Replay is available for up to 30 days, and can expire earlier when source-data retention ends or the source is deleted; the UI exposes the effective `replayExpiresAt`.

Endpoint creation, reactivation, or tier upgrade does not backfill historical events or past rule matches. Test interactions (`isTest`) and `MOCK` mode do not emit lifecycle events. A scenario dry-run never sends an automation event. The endpoint UI test action sends synthetic, no-PII fixtures using the same three lifecycle event types.

## Endpoint verification

Verification is a signed control message, not a CloudEvent or a subscribable event. Betterstood sends `Content-Type: application/json` with the exact body shape `{ "challenge": "<random-128-bit-value>" }`. Verify its `Elicitra-Signature` over the raw body like any event delivery. Activation requires both a 2xx delivery response and pasting the observed challenge back into Betterstood.

Endpoint URLs must use HTTPS on port 443. The URL is immutable; changing it creates and verifies a new endpoint.

## Payload boundary and truncation

The maximum serialized payload is 256 KiB. Identifier, key, and label values are at most 256 characters; email is at most 320 characters; phone is at most 32 characters; business text is at most 8 KiB of UTF-8; collections contain at most 100 elements.

When optional content exceeds the limit, Betterstood deterministically removes collection elements from the tail and truncates text at a valid UTF-8 boundary. `data.truncation` is always present, and its `omittedFields` contains JSON pointers for affected fields. If even a minimum payload cannot be projected, Betterstood records and repairs a projection gap without converting a successful analysis into `analysis.failed`.

Transcripts, recordings, source quotes, raw model output, provider details, raw errors, debug/validation data, raw consent receipts, and arbitrary lead-card data are always excluded. See [Security and privacy](/developers/security/) and the generated [event reference](/developers/reference/webhook-events/).

## Credits

Lifecycle subscriptions, endpoint configuration and verification, synthetic tests, dry-runs, no-match evaluations, failed attempts, retries, and replay consume 0 credits. A conditional automation consumes 1 activation credit only when its delivery first succeeds; retries and replay do not charge it again.
