Skip to content

Receive messages

Polling for new messages does not scale and is always slightly out of date. Webhooks push each event to you as it happens.

  1. One public HTTPS URL that accepts POST with a JSON body.

    app.post('/hooks/kmessage', express.raw({ type: 'application/json' }), (req, res) => {
    // Verify first, using the raw bytes. See the signature guide.
    if (!verify(req.body, req.headers)) return res.sendStatus(401)
    // Answer immediately, work afterwards. We time out, and a slow
    // endpoint turns into retries and duplicates.
    res.sendStatus(200)
    const { event, data } = JSON.parse(req.body)
    queue.push({ event, data })
    })

    Two rules matter more than anything else here: verify before trusting, and reply before working.

  2. In the dashboard, Settings → Webhooks → Add webhook, or through the API:

    Terminal window
    curl https://k-message.kerneltics.com/v1/webhooks \
    -H "Authorization: Bearer km_live_YOUR_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "name": "Order updates",
    "url": "https://api.example.com/hooks/kmessage",
    "events": ["message.incoming", "message.delivered", "message.failed"]
    }'

    The response carries the signing secret, once. Store it.

  3. Use the Send test button, or POST /v1/webhooks/{id}/test. It reports the status your own server returned and what it said, so a failing endpoint is diagnosable without reading your logs.

  4. switch (event) {
    case 'message.incoming':
    await onCustomerReply(data.contact_phone, data.content)
    break
    case 'message.delivered':
    await markDelivered(data.message_id)
    break
    case 'message.failed':
    await markFailed(data.message_id, data.error_message)
    break
    }

    Ignore events you do not recognise rather than failing on them. New event types are added over time, and a 500 on an unknown one turns an addition into an outage.

A 5xx or a timeout is retried three times with backoff: 1s, 2s, 4s. A 4xx is not retried, because a request that was malformed will still be malformed.

Deduplicate on X-KM-Delivery-Id. All attempts at one event share it, so storing the ids you have processed makes a retry harmless. Assume you will see duplicates: a retry after your server processed the event but before its response reached us is a normal outcome, not an error.

The delivery history is the first place to look, not the last. Open the webhook in the dashboard, or:

Terminal window
curl https://k-message.kerneltics.com/v1/webhooks/{id}/deliveries \
-H "Authorization: Bearer km_live_YOUR_KEY"

Every attempt is there with the exact payload we sent and what your server replied, kept for 7 days. In practice it is nearly always one of:

  • status_code: 0 — we never reached you. DNS, a firewall, or an expired TLS certificate.
  • status_code: 500 — your handler threw. response_body usually contains the stack trace.
  • status_code: 401 — your signature check is rejecting us. See Verify webhook signatures; it is almost always a body that was parsed before being verified.
  • Nothing at all — the event is not in the webhook’s subscription list, or the webhook is switched off.

Your endpoint must be reachable from the internet, so private and loopback addresses are refused. Use a tunnel (ngrok http 3000 or similar) and register the public URL it gives you.