Developers

Webhooks: know what happened to every send

Drop Cowboy® posts a signed JSON event to your server when a voicemail lands, a text fails, a contact replies or an AI agent finishes a call.

Set up in four steps

  1. Subscribe

    Call POST /register/public/webhooks once per event type, or use Developers > Webhooks in the dashboard. Several event types can share one URL.

  2. Store the signing secret

    Each subscription returns a signing_secret. Subscribing to the same event type again replaces the URL and issues a new secret.

  3. Verify every delivery

    X-Signature is sha256= plus an HMAC-SHA256 of the X-Timestamp value, a dot, and the raw body. Verify the raw bytes and reject old timestamps.

  4. Answer fast, work later

    Return any 2xx within 5 seconds and process from a queue. Deduplicate on event_id, which is the same on every attempt.

Subscribe and verify

Subscribe to voicemail results
curl -X POST https://api-v2.dropcowboy.com/register/public/webhooks \
  -H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "hook_type": "contact.rvm.status",
    "hook_url": "https://hooks.example.com/dropcowboy"
  }'
Verify a delivery (Node.js)
const crypto = require('crypto');

function verifyWebhook(rawBody, headers, secret) {
  const signature = headers['x-signature'];
  const timestamp = headers['x-timestamp'];
  if (!signature || !timestamp) return false;

  // Reject deliveries signed more than 5 minutes ago.
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return false;

  const expected = 'sha256=' + crypto.createHmac('sha256', secret)
    .update(timestamp + '.')
    .update(rawBody)
    .digest('hex');
  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Events you can subscribe to

GET /register/public/events returns the live list.

Voice sends

contact.rvm.status for ringless voicemail, voice broadcast and AI calls; contact.rvm.receipt for proof of delivery.

Texts and replies

contact.sms.status for every text, contact.msg.received for inbound messages and contact.msg.opt_out when a contact replies STOP.

Email

contact.email.status for delivery, opens and clicks, plus bounce, hard bounce and complaint events.

AI agents

ai_agent.call.completed, ai_agent.call.failed and ai_agent.outcome.* events when an agent finishes a call.

Campaigns

campaign.started, campaign.paused and campaign.completed, alongside the per-contact status events.

Contacts and more

Contact, list, consent, call, appointment, form, pipeline and chat events. Bulk changes arrive as one event per operation.

Webhook questions

How are failed deliveries retried?
A delivery is tried at most 3 times: immediately, about 1 minute later and about 5 minutes after that. Timeouts, connection errors, 408, 429 and 5xx are retried; other 4xx responses are not. After the third try the event is dropped.
What if my endpoint keeps failing?
Deliveries to that origin pause after 100 retriable failures in a row or 5 rejections in a row. Pauses last 5, 15, 30, then 60 minutes, and events that occur during a pause are dropped, so fix the endpoint and reconcile from the API.
Can I receive the same event twice?
Yes. Use event_id to skip duplicates; it is the same on every attempt and for every subscription that receives it.
Does a 202 from a send mean it was sent?
No. A 202 means accepted. Wait for the channel's status event, which carries status, reason and reason_code, before you report a send as sent or failed.
What about callback_url?
Send routes also accept a callback_url. It is not signed and is tried once with a 10-second timeout, so use signed webhooks for anything that matters.
Are Detection webhooks the same?
No. Detection sessions send their own webhooks, configured per session and signed with your Detection API key using different headers. See the Detection docs.

Ready to wire up results?