Conerix

Conerix API

Home
Browse topics

Webhooks

Conerix sends webhooks to your application as message status changes. Know instantly when a message is sent, delivered, or fails.

Event types

EventSent when
message.sentThe upstream provider has accepted the message.
message.deliveredThe carrier has confirmed delivery to the handset.
message.failedThe message could not be delivered (provider error, blocklisted, undeliverable).

Create a webhook endpoint

post /v1/webhooks/create

curl https://conerix.com/api/v1/webhooks/create \
  -H "Authorization: Bearer ds_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/conerix",
    "description": "Production delivery receipts",
    "events": ["message.sent", "message.delivered", "message.failed"]
  }'

The response includes the signing_secret — store it. It is shown only on creation.

{
  "success": true,
  "data": {
    "id": "whe_aKjf91zXq2pLm0c4d3eR5sNbY",
    "object": "webhook_endpoint",
    "url": "https://example.com/webhooks/conerix",
    "description": "Production delivery receipts",
    "events": ["message.sent", "message.delivered", "message.failed"],
    "is_active": true,
    "signing_secret": "whsec_3f7b9a2e4d..."
  }
}

Payload shape

Every webhook POSTs a JSON event of this form:

{
  "id": "evt_a9bX2mF4tQpKrLcSdN1zVeY3o",
  "object": "event",
  "type": "message.delivered",
  "created_at": "2026-05-12T11:34:23+00:00",
  "data": {
    "id": "msg_VyB2pNkX0wnA9aTrLqDh1Z3Fc",
    "object": "message",
    "to":   "+12025550143",
    "from": "Conerix",
    "status": "delivered",
    "delivered_at": "2026-05-12T11:34:23+00:00"
  }
}

Verifying signatures

Every webhook request includes a signature header:

Conerix-Signature: t=1715512463,v1=8c1a...d4f7

v1 is the lowercase hex HMAC-SHA-256 of {timestamp}.{raw_request_body} using your endpoint's signing secret. Verify the signature before parsing the body.

import crypto from "node:crypto";
import express from "express";

const app = express();

// IMPORTANT: keep the raw body for signature verification.
app.post(
  "/webhooks/conerix",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signatureHeader = req.header("Conerix-Signature");
    const [tsPart, sigPart] = signatureHeader.split(",");
    const timestamp = tsPart.split("=")[1];
    const signature = sigPart.split("=")[1];

    const expected = crypto
      .createHmac("sha256", process.env.CONERIX_WEBHOOK_SECRET)
      .update(`${timestamp}.${req.body.toString()}`)
      .digest("hex");

    if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
      return res.status(400).send("bad signature");
    }

    const event = JSON.parse(req.body.toString());

    switch (event.type) {
      case "message.sent":      console.log("sent:",      event.data.id); break;
      case "message.delivered": console.log("delivered:", event.data.id); break;
      case "message.failed":    console.log("failed:",    event.data.id, event.data.error); break;
    }

    res.sendStatus(200);
  }
);

app.listen(3000);
import os, hmac, hashlib, json
from flask import Flask, request

app = Flask(__name__)
SECRET = os.environ["CONERIX_WEBHOOK_SECRET"].encode()

@app.post("/webhooks/conerix")
def conerix_webhook():
    raw = request.get_data()  # bytes — DO NOT use request.json() before verifying.
    signature_header = request.headers.get("Conerix-Signature", "")
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    timestamp, signature = parts.get("t", ""), parts.get("v1", "")

    expected = hmac.new(
        SECRET,
        f"{timestamp}.{raw.decode()}".encode(),
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected, signature):
        return "bad signature", 400

    event = json.loads(raw)
    if event["type"] == "message.delivered":
        print("delivered:", event["data"]["id"])
    return "", 200
Reject stale timestamps. If t is more than 5 minutes from your server's clock, reject the event. This defends against captured-payload replay attacks.

Retry policy

If your endpoint does not return a 2xx within 10 seconds, Conerix retries with exponential backoff:

AttemptSchedule
1Immediate
2+ 30s
3+ 5m
4+ 30m
5+ 2h
6+ 12h

After 6 failed attempts, the event is marked abandoned. After 25 consecutive failed deliveries to the same endpoint, the endpoint is automatically disabled — you'll see is_active: false and a disabled_at timestamp.

Inspecting events

List recent events your account has produced:

get /v1/events?type=message.delivered

Best practices

  • Acknowledge fast. Return 200 within 10s — do heavy work asynchronously.
  • Be idempotent. The same event id can be redelivered. Dedupe on id.
  • Verify every time. Never trust unsigned or stale payloads.
  • Use HTTPS, on the public internet. Plain HTTP endpoints are accepted but not recommended.