Deliveries are processed by an asynchronous queue. Your application code never blocks on a delivery — Mapping.Travel enqueues a delivery row in the same database transaction that emits the event, and a background worker drains the queue.
This behaviour is identical for all three destination types (Webhook, S3, SFTP).
Lifecycle
job completes / event fires
│
▼
PENDING ──── success ────▶ SUCCESS
│
│ failure (any)
▼
PENDING (attempt 2, retry after 1m)
│
▼
PENDING (attempt 3, retry after 5m)
│
▼
PENDING (attempt 4, retry after 30m)
│
▼
DEAD (audit-logged, no more attempts)
Retry schedule
| Attempt | Delay before next try |
|---|
| 1 (initial) | — |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5+ | Marked DEAD |
Total window before dead-lettering: ~36 minutes.
Mapping.Travel does not implement infinite retries. If your endpoint or bucket is unavailable for hours, dropping events is preferable to building an unbounded backlog.
Failure classes
Different failure types produce different audit-log reason codes:
| Failure class | Examples | Reason code |
|---|
| Auth failure | HTTP 401, 403; S3 AccessDenied; SFTP Authentication failed | REPEATED_AUTH_FAILURE |
| Server error | HTTP 5xx; S3 InternalError; SFTP I/O error | REPEATED_5XX |
| Network error | DNS failure, connection refused, TLS error, timeout | REPEATED_NETWORK_FAILURE |
| Host key mismatch | SFTP HOST_KEY_MISMATCH | REPEATED_NETWORK_FAILURE |
After 4 failed attempts, the delivery is marked DEAD and the reason code is written to the audit log.
Auto-disable
If a destination accumulates 5 dead deliveries within any 24-hour window, it is automatically set to INACTIVE. No further deliveries are attempted until you re-enable it.
To re-enable a destination:
curl -X PATCH \
https://api.mapping.travel/api/v1/delivery-destinations/{id} \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{ "status": "ACTIVE" }'
Alternatively, run the connection test (POST /api/v1/delivery-destinations/{id}/test). A successful test auto-reactivates a disabled destination.
Response handling (webhooks)
| Response | Outcome |
|---|
2xx | SUCCESS, no further attempts |
3xx | Treated as failure — webhooks must respond inline, not redirect |
4xx | Failure, retried |
5xx | Failure, retried |
| Connection refused / DNS error / timeout (>30s) | Failure, retried |
Idempotency (webhooks)
Every event has a stable eventId field reused across every retry. The delivery id changes per attempt.
Use eventId as the dedup key:
async function handle(payload: WebhookPayload) {
const seen = await db.exists(`webhook:${payload.eventId}`);
if (seen) return; // already processed
await processEvent(payload);
await db.set(`webhook:${payload.eventId}`, '1', { ex: 86400 });
}
Idempotency (S3 and SFTP)
S3 and SFTP deliveries write a file with a deterministic name. If a delivery is retried, the file is overwritten with identical content — the result is the same whether one or three attempts succeed.
Debugging
List recent delivery attempts for a destination:
curl "https://api.mapping.travel/api/v1/delivery-destinations/{id}/deliveries?limit=50" \
-H "Authorization: Bearer <your-token>"
Each entry includes status, attemptCount, lastError, and timestamps. In the app, open Developers → Delivery destinations → [destination name] → Deliveries.
Payload size limit (webhooks)
Webhook payloads are capped at 64 KB. If a single event’s data exceeds the cap, the data field is replaced with { "truncated": true, "dataRef": "delivery/<id>" }. Query the API for the full state if you encounter this.
Backpressure
If an organization accumulates more than 10,000 PENDING deliveries, new deliveries are dropped silently and an hourly audit-log entry records the back-pressure event. Once the queue drains below the threshold, normal delivery resumes.