Webhooks
Get a signed call to your server when a reply comes in, someone is interested, an email bounces, a list or an export is ready. Included from the Pro plan.
Events
| Event | When | What data holds |
|---|---|---|
reply.received | A person replied to one of your campaigns. Out-of-office and other automatic replies are left out | classification, lead_email, campaign_id, subject, snippet |
reply.positive | A reply was labelled interested | The same as above |
email.bounced | A campaign email bounced | lead_email, campaign_id, subject |
email.unsubscribed | Someone asked to stop, complained, or used the opt-out link | classification, lead_email, campaign_id, subject, snippet, and source for the opt-out link |
campaign.paused | Sending paused: the month's campaign emails of your plan are used up, or the campaign paused itself (for example too many bounces) | campaign_id, campaign_name, reason, and campaign_emails (plan) or message, metrics, url (auto-pause) |
campaign.resumed | The month started again or the plan grew, so sending goes on | campaign_id, campaign_name, reason, campaign_emails |
export.ready | An export finished | job_id, status, total_rows, verified_rows, status_url, download_url |
list.ready | A list finished filling, also after you added more people | list_id, list_name, job_id, status, total_items, note, status_url |
verification.completed | A single email check with an API key finished | email, status, deliverable, is_role_based, is_free, is_disposable |
segment.new_matches | A saved list has new people who match its filters | list_id, list_name, previous_total, current_total, new_matches |
Campaign events go to the webhooks of the workspace owner and of every active team member, so each person can point them at their own tools. Exports, lists and checks go to the webhooks of the person who started them.
Create a webhook
curl -X POST "https://app.argorant.com/api/v1/webhooks" \
-H "Authorization: Bearer $ARGORANT_API_KEY" -H "Idempotency-Key: hook-crm-1" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/hooks/argorant","events":["reply.positive","email.bounced","export.ready"]}'The address must be a public https:// address. The answer holds the signing secret. It is shown only once, so store it now. Each account can have up to 10 webhooks, switched off ones included.
| Call | What it does |
|---|---|
GET /api/v1/webhooks | Your webhooks and every event you can choose |
PATCH /api/v1/webhooks/{id} | Change the URL or the events, or switch it off with "active": false |
DELETE /api/v1/webhooks/{id} | Remove it |
POST /api/v1/webhooks/{id}/test | Send one signed test call |
GET /api/v1/webhooks/{id}/deliveries | Recent calls with their result and the next try |
What your server receives
POST /hooks/argorant
Content-Type: application/json
User-Agent: Argorant-Webhooks/1.0
X-Argorant-Event: reply.positive
X-Argorant-Delivery: 8841
X-Argorant-Signature: sha256=5d41402abc4b2a76b9719d911017c592...
{"event": "reply.positive", "delivery_id": "8841", "created_at": "2026-10-07T09:14:03+00:00",
"data": {"classification": "positive", "lead_email": "jane.doe@acme.com", "campaign_id": "7f3a2c1e-9b4d-4e8a-a1c2-5d6e7f8a9b0c", "subject": "Re: Quick question", "snippet": "Happy to talk..."}}Answer with any 2xx status within a few seconds. Do the slow work after you answered. Redirects are not followed, so use the final address.
Check the signature
The signature is the HMAC-SHA256 of the raw body with your secret. Compare it before you trust the call. Use the body bytes exactly as they arrived, before any JSON parsing.
import hmac, hashlib
def is_from_argorant(secret: str, raw_body: bytes, signature_header: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header)const crypto = require("crypto");
function isFromArgorant(secret, rawBody, signatureHeader) {
const expected = Buffer.from("sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex"));
const given = Buffer.from(signatureHeader || "");
return given.length === expected.length && crypto.timingSafeEqual(expected, given);
}The signed body holds delivery_id and created_at, so neither can be changed without breaking the signature. Keep the delivery ids you handled and skip one you have seen before. That makes a replayed call harmless.
When your server is down
A call that fails is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, so up to 6 times. X-Argorant-Delivery stays the same for every try of one event, so use it to skip events you already handled. After 50 failed calls in a row the webhook is switched off; switch it on again with PATCH and "active": true.