Delivery & Retries
Delivery guarantees
SkyLight Chat delivers webhooks at least once. In rare cases of network issues or ambiguous responses, the same event may be delivered more than once. Use the delivery_id field to deduplicate events in your system.
// Example: idempotent event processing
async function handleEvent(payload) {
const { delivery_id, event, data } = payload
const alreadyProcessed = await db.deliveries.exists({ delivery_id })
if (alreadyProcessed) return // skip duplicate
await db.deliveries.create({ delivery_id, processed_at: new Date() })
// proceed with processing...
}
Delivery process
- An event is triggered in SkyLight Chat
- A delivery log entry is created with
status: pending - An async job is queued to send the
POSTrequest - If the response is
2xxwithin 30 seconds, the delivery is markeddelivered - If not, the delivery is marked
failedand scheduled for retry
Response requirements
Your endpoint must:
- Respond with a
2xxHTTP status code (200,201,202,204, etc.) - Respond within 30 seconds
- Be accessible over HTTPS
If your endpoint returns a non-2xx status or times out, SkyLight Chat will retry.
Retry schedule
Failed deliveries are retried up to 3 times with exponential backoff:
| Attempt | Delay after failure |
|---|---|
| 1st retry | 2 minutes |
| 2nd retry | 10 minutes |
| 3rd retry | 30 minutes |
After 3 failed attempts the delivery is permanently marked failed. No further retries occur. You can inspect failed deliveries in the dashboard or via the delivery logs API.
Delivery status
| Status | Description |
|---|---|
pending | Queued but not yet attempted |
delivered | Received a 2xx response |
failed | All attempts exhausted without a 2xx response |
Checking delivery logs
Via the dashboard: Settings → Webhooks → webhook name → Delivery Logs
Via the API:
curl "https://dashboard.skylightchat.com/api/v1/webhooks/7/deliveries?status=failed" \
-H "Authorization: Bearer sk_live_••••••••••••"
{
"success": true,
"data": [
{
"id": 1024,
"event_type": "contact.created",
"delivery_id": "a1b2c3d4-...",
"attempt_number": 3,
"response_status_code": 500,
"response_body": "Internal Server Error",
"status": "failed",
"created_at": "2026-03-04T12:00:00.000000Z"
}
]
}
Endpoint best practices
Respond fast, process async
Return 200 OK immediately, then process the event asynchronously using a queue:
app.post('/webhooks/skylightchat', (req, res) => {
// Verify signature first
verifySignature(req)
// Acknowledge immediately
res.status(200).send('OK')
// Process asynchronously
queue.push(req.body)
})
This ensures you never time out and always acknowledge delivery — even when your processing takes longer than 30 seconds.
Make handlers idempotent
Use the delivery_id to ensure your handler can safely be called multiple times for the same event without side effects.
Use HTTPS
Only HTTPS endpoints are accepted. HTTP endpoints will be rejected at webhook creation time.
Handle all event types
Even if you only care about some events, return 200 OK for all deliveries. A 404 or 405 response looks like a failure to our delivery system and triggers retries.
switch (event) {
case 'contact.created':
await handleContact(data)
break
default:
// Acknowledge events you don't care about
break
}
Disabling a webhook
If your endpoint is temporarily unavailable, disable the webhook in the dashboard to stop deliveries:
curl -X PUT https://dashboard.skylightchat.com/api/v1/webhooks/7 \
-H "Authorization: Bearer sk_live_••••••••••••" \
-H "Content-Type: application/json" \
-d '{"is_active": false}'
Re-enable it when your endpoint is ready. Note that events are not queued while a webhook is disabled.
