Skip to main content
This page states what iSnap guarantees when it delivers an event to your endpoint, and what it does not. Read it before you decide how your receiver deduplicates, orders and recovers events. Each rule is stated here once: the other pages link to it.

What is guaranteed

  • At least once, while the subscription stays active. iSnap sends a recorded event to each subscription it matched until your endpoint returns 2xx or 410, or the attempts run out. A delivery can end before its attempts run out: when the subscription is turned off or deleted, when its URL is refused, when iSnap would resend it after a suspension or a stall more than 24 hours after its creation, and when it waits or stalls again after iSnap resent it 3 times. See Success and failure and Suspension. A delivery that ends failed can be replayed while its record exists, and its event read from the events feed. An event can be delivered more than once.
  • An event is recorded only if it matches an active subscription when it occurs. An event that matches no active subscription at that moment is never delivered and never listed.
  • An event can be lost: see What events do not announce.
  • No delay is promised. The API does not bound the delay between a change and the first attempt.
The resource operations are the source of truth. Use events to act fast, and reconcile from the API: see What events do not announce.

Duplicates

Deduplicate on event_id. It is the same value as the webhook-id header, and it is the same on every attempt, on a replay, and for every subscription that receives the event. The same event reaches you more than once when:
  • your endpoint processed a request but returned its response after 10 seconds;
  • iSnap resends a delivery after a suspension ends, or after it recovers a delivery that stalled;
  • you replay a delivery;
  • two of your subscriptions have the same URL and both match the event;
  • a line that is not a BYOD line stays offline: its durable line.offline is delivered again with the same event_id.
Never deduplicate on a hash of the body. The first attempt and its scheduled retries send the same bytes, but a replay, or a delivery that iSnap resends after a suspension or a stall, can order the JSON keys differently. The content is the same, and the signature covers the bytes of each request.

Order

Delivery order is not guaranteed. Order events by the timestamps inside data, not by arrival. For message events, apply Building the status from events. Deliveries are sent concurrently and retried independently, so events can arrive in any order, two events about the same message included. The created_at of the envelope is the time iSnap recorded the event, not the time the change happened.

Success and failure

An attempt succeeds when your endpoint returns a status from 200 to 299 within 10 seconds. iSnap does not interpret the response body, but it keeps its first 1,024 characters in the delivery record, readable with any API key of your account: return an empty body. Every other outcome is a failed attempt:
  • a 3xx status: iSnap never follows a redirect;
  • a 4xx status, 400, 401, 404 and 429 included;
  • a 5xx status;
  • no response within 10 seconds;
  • a host that does not resolve, a connection error, or a TLS error.
A failed attempt is retried, except after a 410. iSnap ignores a Retry-After header. Four outcomes end a delivery without another attempt:

Retries

A retry schedule has at most 7 attempts. Each delay is counted from the previous attempt. A delivery that iSnap resends after a suspension or a stall starts a new schedule, so one delivery can have more than 7 attempts in all. The delays add up to 14 hours 36 minutes 30 seconds, so the 7th attempt starts at least that long after the first.
  • Every attempt carries the same webhook-id and a new webhook-timestamp and signature.
  • A new url set with PATCH /v1/webhooks/{id} applies to every request not sent before the change, pending retries included.
  • When the 7th attempt fails, the delivery ends failed and the failure_count of the subscription rises by 1.
  • A delivery that stays pending with no attempt scheduled is resent by iSnap, with a full new schedule, if it was created less than 24 hours earlier. Past 24 hours it ends failed. It can also end failed when it stalls again after iSnap resent it 3 times. In both cases its response_body_snippet starts with abandoned:, and you can replay it while its record exists.
An outage of your endpoint shorter than 14 hours 36 minutes 30 seconds does not by itself suspend the subscription: the deliveries created during the outage keep their remaining attempts, on the schedule above. Read the events feed to catch up without waiting for them.

Suspension

iSnap suspends a subscription in two cases: failure_count counts deliveries, not attempts. One successful delivery, or one successful probe, resets it to 0. last_success_at is the time of the last 2xx, from a delivery or a probe. last_failure_at is the time of the last delivery that used all its attempts, the last 410, or the last refused URL. Both are null until the first such outcome. These three fields are returned by POST /v1/webhooks and GET /v1/webhooks only.

While a subscription is suspended

  • A delivery that was already created waits: no request is sent, its status stays pending, and its next_retry_at becomes null.
  • iSnap sends a probe: a signed webhook.test request. Two probes are at least 15 minutes apart. The first is sent within 15 minutes of the suspension, unless the subscription was already probed in the 15 minutes before it.
  • A subscription whose URL is refused is not probed until the URL passes the check again: after you change it with PATCH /v1/webhooks/{id}, or after its host resolves to a public address again.
The events that occur while a subscription is suspended are lost for it. They are not recorded, they are not delivered after it resumes, they cannot be replayed, and they are absent from the events feed unless another active subscription of your account matched them. After a suspension, reconcile from the resource operations.

How a suspension ends

A 2xx response to a probe, received within 10 seconds, resumes the subscription: failure_count returns to 0 and last_success_at is set. Any other response, a redirect, a timeout or a network error leaves it suspended. No operation resumes a suspended subscription. PATCH /v1/webhooks/{id} with active: true, and a change of URL, do not end a suspension: only a successful probe does. After the subscription resumes, iSnap resends each delivery that waited, with a full new retry schedule that starts at attempt 1. The API does not bound the delay between the resume and the resend. There are two exceptions:
  • a delivery created more than 24 hours before the time iSnap would resend it ends failed, with response_body_snippet set to abandoned: older than 24h;
  • a delivery that iSnap already resent 3 times can end failed instead, with response_body_snippet set to abandoned: rescued 3 times.
You can replay such a delivery while its record exists. The events that occurred during the suspension are not sent.

How to detect a suspension

No field says that a subscription is suspended, and no event announces it. A suspended subscription still reads active: true. Use these signs: To avoid a suspension, monitor the failures of your endpoint, and check that the host of your URL keeps resolving to a public address.

A 410 response

A 410 response to a delivery ends that delivery failed and sets the subscription to active: false immediately. It does not change failure_count. An inactive subscription is not probed. Turn it on again with PATCH /v1/webhooks/{id} and active: true. The events that occurred while it was inactive are lost for it. A 410 response to a probe has no special effect: the subscription stays suspended.

Turning off and changing a subscription

PATCH /v1/webhooks/{id} accepts any subset of url, events, line_ids, description and active. A body with none of them returns 400 validation_failed.

Replays

POST /v1/webhooks/{id}/deliveries/{delivery_id}/replay creates a new delivery of the event of a finished delivery. A replay targets a delivery of a subscription, not an event id. A replay records a new delivery of the event at the time of the replay. The events feed lists an event at the time of its earliest delivery inside your window: while the first delivery is inside the window, a replay does not move the event; otherwise the event is listed at the time of the replay, with its original created_at. Deduplicate on event_id. The sample replays every event of the last 24 hours whose latest delivery failed. The list returns the newest record first, so the first record seen for an event_id is its latest delivery.
A replay does not honor Idempotency-Key: a call sent twice creates two deliveries of the same event. Your deduplication on event_id absorbs them.

Secret rotation

POST /v1/webhooks/{id}/rotate-secret replaces the signing secret and returns 200 with id, secret and old_secret_expires_at.
  • The rotation takes effect immediately. Every request sent after it is signed with the new secret only: first attempts, retries of deliveries created before the rotation, replays and probes.
  • There is no overlap. After a rotation iSnap never signs with the previous secret, and it never sends two signatures.
  • old_secret_expires_at is informational. It is the time of the rotation plus 5 minutes, and it has no effect on what iSnap sends.
  • The new secret is returned once. No other operation returns it.
Rotate in this order, so that no delivery is refused for long:
1

Make your receiver accept a list of secrets

Verify each request against every secret of the list, and accept it when one matches. With the SDK, pass an array as the third argument of verifyWebhook.
2

Rotate, and store the new secret

Call the operation once. Do not retry it blindly: it does not honor Idempotency-Key, and a second call replaces the secret again. If you lose the response, rotate again.
3

Add the new secret to the list immediately

Between the rotation and this step, your receiver cannot verify the requests iSnap sends. Return a status outside 2xx for a signature you cannot verify: iSnap retries the delivery on the schedule.
4

Remove the previous secret after old_secret_expires_at

Keeping the previous secret until then covers a request that iSnap signed just before the rotation and that is still in transit.
The response contains the signing secret: run the command where its output is not logged, and store the secret in your secret manager.

Delivery records

GET /v1/webhooks/{id}/deliveries lists the delivery records of a subscription. A record describes one delivery: one event for one subscription. It is updated at each attempt, so it shows the latest attempt, not a history of attempts. In four cases where no request was sent, response_body_snippet contains a note written by iSnap, not a response of your endpoint: A delivery whose URL was refused, or whose host did not resolve, has no note: its response_body_snippet is null. The list takes these parameters: Records are returned newest first. has_more says whether another page exists: next_cursor is then a string, and null otherwise. A malformed cursor returns 400 invalid_cursor. The cursor does not carry the filters: send the same filters on every page. See Cursors. A probe creates no delivery record. request_body_snippet can contain personal data: the beginning of an event, with message text and handles. Do not copy delivery records into logs, tickets or other tools.

Retention

During these periods, anyone who has an API key of your account, a line-scoped key included, can read the full content of each event through the events feed, and the first 1,024 characters of each request body and of each response of your endpoint through the delivery list.

Deleting a subscription

DELETE /v1/webhooks/{id} returns 204 with no body.
Deleting a subscription is permanent. Its secret and its delivery records are deleted with it, the requests not sent are dropped, and the events that only this subscription received leave the events feed. To stop deliveries and keep the records, set active: false instead.