SkyLight Chat
Webhooks Overview

Delivery & Retries

How SkyLight Chat delivers webhook events, handles failures, and 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

  1. An event is triggered in SkyLight Chat
  2. A delivery log entry is created with status: pending
  3. An async job is queued to send the POST request
  4. If the response is 2xx within 30 seconds, the delivery is marked delivered
  5. If not, the delivery is marked failed and scheduled for retry

Response requirements

Your endpoint must:

  • Respond with a 2xx HTTP 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:

AttemptDelay after failure
1st retry2 minutes
2nd retry10 minutes
3rd retry30 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

StatusDescription
pendingQueued but not yet attempted
deliveredReceived a 2xx response
failedAll 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.