Webhooks
Get a signed message at your own URL, or in Slack, the moment something happens in RecruitAffiliates.ai.
A webhook is a message we send to a web address you choose when something happens, like a reply or a finished search. Your code at that address can then act on it straight away.
Webhooks come with the Grow and Enterprise plans. Billing has the plans.
Add an endpoint
An endpoint is the web address that receives the messages.
- Go to Settings, then the Webhooks tab.
- Add an endpoint and paste your URL. It must be a public
https://address. - Pick the events you want. The full list is below.
- Copy the signing secret. We show it once. If you lose it, rotate it.
We refuse an address that points at a private or internal network, both when you save it and again each time we send. We never follow a redirect.
What we send
We send each event within a minute of it happening. Each event is an HTTP POST with a JSON body. Here's the shape:
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: RecruitAffiliates-Webhooks/1.0
Rail-Signature: t=1790415000,v1=5f1c6c...e2a9
Rail-Event: reply.received
Rail-Event-Id: fev_1AbCdEfGhIjKlMnOpQrStU
Rail-Delivery-Id: fwd_7HgFdSaQwErTyUiOpLkJh
Rail-Attempt: 1
webhook-id: fev_1AbCdEfGhIjKlMnOpQrStU
webhook-timestamp: 1790415000
webhook-signature: v1,g0hM9SsE...JEE=
{"id":"fev_1AbCdEfGhIjKlMnOpQrStU","type":"reply.received","version":1,"created_at":"2026-09-26T09:30:00.000Z","livemode":true,"data":{...}}| Header | What it tells you |
|---|---|
Rail-Signature | Proof the message came from us. See Check the signature. |
Rail-Event | The event name, the same as type in the body. Handy for routing before you parse. |
Rail-Event-Id | The event id. It stays the same across retries and replays. |
Rail-Delivery-Id | This one send. A replay gets a new delivery id and keeps the event id. |
Rail-Attempt | 1 for the first try, up to 8. |
webhook-id, webhook-timestamp, webhook-signature | The same proof in Standard Webhooks form, for off-the-shelf libraries. |
The envelope
The envelope is the outer layer of the body. It's the same for every event:
| Key | Type | Meaning |
|---|---|---|
id | string | The event id. Test events start with evt_test_. |
type | string | The event name, from the list below. |
version | number | The payload version. Every event is version 1 today. We won't change a payload's keys without a new version. |
created_at | string | When it happened, in ISO 8601 UTC. |
livemode | boolean | false for a test event. See Test mode. |
data | object | The event's own fields, shown per event below. |
Inside data, ids are prefixed strings like fdi_ or fca_. A prospect is always prospect_id. Times are ISO 8601 UTC. A field with nothing in it comes as null, so every event of one type has the same keys.
We never put an email body, an email address, a typed search or a deal's terms in a webhook. You get the ids. Fetch the full record from the API when you need it.
Events
Each event has a name like reply.received. Pick only the ones you need.
| Event | What happened |
|---|---|
affiliate.added | Someone added a prospect by hand. |
affiliate.saved | Someone saved a prospect, to a list or to their saved affiliates. |
affiliate.hidden | Someone hid a prospect, with the reason they picked. |
affiliate.stage_changed | A prospect moved to another pipeline stage, by hand or because of outreach. by_user_id is null when outreach moved it. |
email.revealed | An email address was found for a prospect. Fetch the prospect from the API to read the address. |
affiliate.unsubscribed | A prospect used the unsubscribe link. Every sequence to them has stopped. |
do_not_contact.added | Someone marked a prospect Do not contact. No outreach will reach them. |
do_not_contact.lifted | Someone lifted a Do not contact mark. |
search.completed | A search you ran from Find new finished. state is done, or stopped_at_ceiling when it hit its spending cap. |
search.failed | A search couldn't start. The search credit was refunded. |
scan.completed | A scan finished: the first one, the weekly one, or the scan behind a search. |
scan.failed | A scan failed. error says why in plain words, and credit_refunded says whether a search credit came back. |
message.sent | An outreach email left your inbox. step_index is 0 for the first email, 1 for the first follow-up. |
message.bounced | An outreach email bounced. The sequence to that address has stopped. |
message.failed | Your mail provider refused an outreach email. |
reply.received | A prospect replied to your outreach. reply_kind is how we read the reply: positive, question, not_now, negative, wrong_person or angry. Interested means positive or question. Out-of-office replies, bounces and unsubscribes have their own events, or none. |
sequence.completed | Every email in a sequence went out and the prospect never replied. |
campaign.launched | A campaign started sending. |
campaign.paused | A campaign stopped sending. reason is bounce_rate or spam_complaint when we paused it to protect your inbox, and null when you paused it yourself. When we pause it, fix says in one sentence what to do. A bounce_rate pause also has sent and bounced, counted since our last pause. If the campaign can't carry on, for example your plan can't send or every inbox is gone, reason says that instead. |
campaign.inbox_dropped | One of a campaign's inboxes disconnected. Only the sequences sending from that inbox paused. The campaign keeps sending from its other inboxes, and the paused ones pick up again when you reconnect it. |
campaign.resumed | A paused campaign started sending again. |
campaign.stopped | Someone stopped a campaign for good. |
campaign.finished | Every prospect in a campaign has finished their sequence. |
campaign.send_failed | A campaign couldn't send to one prospect. The rest of the campaign carries on. |
deal.agreed | You and a prospect agreed a deal. The terms are frozen at version; fetch the deal to read them. |
affiliate.activated | An agreed deal became a live partner in AffiliateRail, with a commission rule on your program. |
inbox.connected | A mailbox was connected for sending. |
inbox.disconnected | A mailbox disconnected. Its sequences and campaigns paused until you reconnect it. |
affiliate.assigned | A prospect got a new owner on your team, or lost one. assignee_member_id is null when nobody owns them now. Member ids match GET /team. by_member_id is null when the API or the system did it. |
note.added | Someone on your team added a note to a prospect. The note's text isn't in the event. Read it with the prospect's notes endpoint in the API. |
affiliate.invited | Someone marked a prospect invited to your affiliate program. |
affiliate.signed_up | An invited prospect signed up as your affiliate. source is manual when your team marked it, or affiliaterail when we saw them join your AffiliateRail program. |
Check the signature
Anyone can post to your URL. The signature proves a message came from us and wasn't changed on the way. Check it before you trust anything in the body.
The Rail-Signature header looks like t=1790415000,v1=5f1c.... Here's how to check it:
- Split the header on commas. Take
t, a Unix time in seconds, and everyv1value. - Build the signed text:
t, a full stop, then the raw body exactly as it arrived. Don't parse and re-write the JSON first, because that changes it. - Work out HMAC-SHA256 of that text, using your whole secret as the key,
whsec_and all. Turn it into hex. - Compare it with each
v1value, in constant time. Any match means it's real. - Reject it if
tis more than five minutes from your clock. That stops someone from sending an old message again.
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
export function verifyRailSignature(secret, header, rawBody, toleranceSeconds = 300) {
const parts = header.split(",").map((p) => p.trim());
const t = parts.find((p) => p.startsWith("t="))?.slice(2);
const sigs = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3));
if (!t || !/^\d+$/.test(t) || sigs.length === 0) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return sigs.some(
(s) => s.length === expected.length && timingSafeEqual(Buffer.from(s, "hex"), Buffer.from(expected, "hex")),
);
}
const app = express();
// Keep the raw body. express.json() would re-write it and the check would fail.
app.post("/webhooks/recruit-affiliates", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!verifyRailSignature(process.env.RA_WEBHOOK_SECRET, req.get("Rail-Signature") ?? "", raw)) {
return res.status(400).send("bad signature");
}
const event = JSON.parse(raw);
res.sendStatus(200); // answer first, then do the slow work
if (event.type === "reply.received") {
// look up event.data.prospect_id and tell your team
}
});
app.listen(3000);import hashlib
import hmac
import os
import time
from flask import Flask, abort, request
app = Flask(__name__)
def verify_rail_signature(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
parts = [p.strip() for p in header.split(",")]
t = next((p[2:] for p in parts if p.startswith("t=")), None)
sigs = [p[3:] for p in parts if p.startswith("v1=")]
if t is None or not t.isdigit() or not sigs:
return False
if abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in sigs)
@app.post("/webhooks/recruit-affiliates")
def recruit_affiliates_webhook():
raw = request.get_data() # the raw bytes, before any JSON parsing
if not verify_rail_signature(os.environ["RA_WEBHOOK_SECRET"], request.headers.get("Rail-Signature", ""), raw):
abort(400)
event = request.get_json()
if event["type"] == "deal.agreed":
pass # fetch the deal by event["data"]["deal_id"]
return "", 200Using a Standard Webhooks library
Every message also carries the three Standard Webhooks headers, signed with the same secret. If your stack has a Standard Webhooks library, hand it the secret, the headers and the raw body, and it does the check for you.
webhook-idis the event id. It stays the same across retries and replays, which is what the library uses to spot a repeat.webhook-timestampis the same Unix time astinRail-Signature.webhook-signatureisv1,then a base64 HMAC-SHA256 of{id}.{timestamp}.{raw body}.
The key works a little differently here. These libraries drop the whsec_ part of the secret and base64-decode the rest before they sign. We sign these headers the same way, so the library works as it is. Rail-Signature keeps using the whole secret.
Answer fast, or we retry
Answer with any 2xx status within 10 seconds. If the work is slow, answer first and do it after.
Anything else counts as a failed try. That means a 3xx, 4xx or 5xx, a timeout or a refused connection. We try again on this schedule, counted from the try before:
| Try | Waits |
|---|---|
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 12 hours |
| 7 | 24 hours |
| 8 | 24 hours |
That's eight tries over about 63 hours, which covers a weekend outage. The waits are the least we wait, so a retry can land a little later. After the eighth failure the delivery is marked failed and stays in the log, where you can replay it.
You may get the same event twice, for example when a network drop hides your 200 from us. Treat id as the key and skip one you've already handled.
When we switch an endpoint off
Let's say your endpoint went away for good. We don't keep failing at it forever. If the last 20 deliveries to an endpoint all failed every try, and those failures span at least five days, we switch it off.
Turn it back on once it works. That, or rotating its secret, starts the count again. The failed deliveries stay in the log so you can replay them.
The delivery log and replays
Each endpoint shows its last 50 deliveries. You see the status, the full request we sent and the response your server gave.
A replay sends a past delivery again. You can replay any delivery from the last 30 days. It goes out as a new delivery with the same body and the same event id, so a handler that skips repeats will skip it. The old entry stays in the log as it was.
To replay one, open the log with Show delivery log, then press Resend on the delivery.
Send a test or a sample event
Press Send a test on the endpoint, or pick an event and press Send a sample. Either one posts a sample of that event to your endpoint, right now. It's signed with your real secret, and you see the status your server gave straight away.
- Its id starts with
evt_test_and itslivemodeisfalse. - The
datais the example from the event list. - It's sent once. A failed test isn't retried.
Use it to check your signature code before a real event arrives.
Rotate the secret
Rotate when a secret leaks, or on a routine you set. Press Rotate secret, then press Yes, rotate. You get a new secret at once, and the old one keeps working for 24 hours.
During those 24 hours, each Rail-Signature has two v1 values, the new secret's first. The code above accepts either one, so nothing breaks while you swap. After 24 hours only the new secret works. Rotating again inside the window replaces the old secret, so at most two are ever live.
Test mode
Each endpoint is either live or test. A test endpoint gets only test events, so you can build against it without real prospects in the way. Test sends and sample events are always test events, with livemode set to false.
You can do all of this with a test API key (one starting rail_test_), so you never need a live key while you build:
- Add an endpoint. A test key always makes a test endpoint.
- Send it a test event of any type, then read the delivery log.
- Turn it off and on, rotate its secret, or delete it.
A test key can't see or change a live endpoint, and it can't replay a delivery.
A test key can also save, hide, move and note the 12 sample prospects. Each of those sends a real event to your test endpoints, marked livemode: false. Nothing else hears about it: your live endpoints, the bell, emails, reports and Activity all skip test events. Use this to run a Zapier, Make or n8n test with real-looking events.
Send events to Slack
You can point an endpoint at a Slack channel. You don't need a Slack app or any sign-in.
- In Slack, create an incoming webhook for the channel. That's a web address Slack gives you that posts into one channel. Slack's guide is at api.slack.com/messaging/webhooks.
- Add an endpoint in RecruitAffiliates.ai, choose Slack as its type, and paste that address.
- Pick your events.
Three events get a proper Slack message:
reply.received: you've got a reply, with the prospect and campaign ids.deal.agreed: you and a prospect agreed terms, with the deal id.search.completed: your search is done, and how many new prospects it found.
Any other event you pick shows as one short line with its name. Slack messages go through the same retries and the same auto switch-off as any endpoint.
Using webhooks with Zapier, Make and n8n
Each of these tools gives you a web address that waits for a message. Add it as an endpoint and you've got a trigger for any workflow. The steps for each tool are in its recipe: Zapier, Make and n8n.
Manage endpoints with the API
You can add, list, test, rotate and delete endpoints with the API too. It's the same as the Webhooks tab, with the same rules.
Example payloads
Every example below is version 1. The ids are made up.
Prospects
affiliate.added
Someone added a prospect by hand.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "affiliate.added",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"channel": "youtube",
"source": "manual_add"
}
}affiliate.saved
Someone saved a prospect, to a list or to their saved affiliates.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "affiliate.saved",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"channel": "website",
"key": "reviewer.com",
"list_id": null
}
}affiliate.hidden
Someone hid a prospect, with the reason they picked.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "affiliate.hidden",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"reason": "not_relevant"
}
}affiliate.stage_changed
A prospect moved to another pipeline stage, by hand or because of outreach. by_user_id is null when outreach moved it.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "affiliate.stage_changed",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"from": "contacted",
"to": "replied",
"by_user_id": null,
"reason": null
}
}email.revealed
An email address was found for a prospect. Fetch the prospect from the API to read the address.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "email.revealed",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"source": "website",
"verification": "valid",
"addresses": 1
}
}affiliate.unsubscribed
A prospect used the unsubscribe link. Every sequence to them has stopped.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "affiliate.unsubscribed",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"campaign_id": "fca_3KlMnOpQrStUvWxYz01234",
"sequence_id": "fsq_5HgFdSaQwErTyUiOpLkJh",
"inbox_id": "fin_6ZxCvBnMaSdFgHjKlQwEr",
"occurred_at": "2026-09-26T09:30:00.000Z"
}
}do_not_contact.added
Someone marked a prospect Do not contact. No outreach will reach them.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "do_not_contact.added",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"reason": "asked_not_to",
"by_user_id": "usr_1QaZxSwEdCvFrTgBnHyUj"
}
}do_not_contact.lifted
Someone lifted a Do not contact mark.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "do_not_contact.lifted",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"reason": "asked_not_to",
"by_user_id": "usr_1QaZxSwEdCvFrTgBnHyUj"
}
}Searches and scans
search.completed
A search you ran from Find new finished. state is done, or stopped_at_ceiling when it hit its spending cap.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "search.completed",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"search_id": "fsc_7WxYz0123456789AbCdEf",
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"search_type": "topic_search",
"state": "done",
"found": 42
}
}search.failed
A search couldn't start. The search credit was refunded.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "search.failed",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"search_id": "fsc_7WxYz0123456789AbCdEf",
"credit_refunded": true
}
}scan.completed
A scan finished: the first one, the weekly one, or the scan behind a search.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "scan.completed",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"scan_id": "fsc_7WxYz0123456789AbCdEf",
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"trigger": "weekly",
"state": "done",
"found": 17
}
}scan.failed
A scan failed. error says why in plain words, and credit_refunded says whether a search credit came back.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "scan.failed",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"scan_id": "fsc_7WxYz0123456789AbCdEf",
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"trigger": "competitor_search",
"error": "No discovery source answered.",
"credit_refunded": true
}
}Outreach
message.sent
An outreach email left your inbox. step_index is 0 for the first email, 1 for the first follow-up.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "message.sent",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"campaign_id": "fca_3KlMnOpQrStUvWxYz01234",
"sequence_id": "fsq_5HgFdSaQwErTyUiOpLkJh",
"inbox_id": "fin_6ZxCvBnMaSdFgHjKlQwEr",
"occurred_at": "2026-09-26T09:30:00.000Z",
"message_id": "fms_8PoIuYtReWqAsDfGhJkLz",
"step_index": 0
}
}message.bounced
An outreach email bounced. The sequence to that address has stopped.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "message.bounced",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"campaign_id": "fca_3KlMnOpQrStUvWxYz01234",
"sequence_id": "fsq_5HgFdSaQwErTyUiOpLkJh",
"inbox_id": "fin_6ZxCvBnMaSdFgHjKlQwEr",
"occurred_at": "2026-09-26T09:30:00.000Z",
"message_id": "fms_8PoIuYtReWqAsDfGhJkLz"
}
}message.failed
Your mail provider refused an outreach email.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "message.failed",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"campaign_id": "fca_3KlMnOpQrStUvWxYz01234",
"sequence_id": "fsq_5HgFdSaQwErTyUiOpLkJh",
"inbox_id": "fin_6ZxCvBnMaSdFgHjKlQwEr",
"occurred_at": "2026-09-26T09:30:00.000Z",
"step_index": 1,
"reason": "provider_rejected"
}
}reply.received
A prospect replied to your outreach. reply_kind is how we read the reply: positive, question, not_now, negative, wrong_person or angry. Interested means positive or question. Out-of-office replies, bounces and unsubscribes have their own events, or none.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "reply.received",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"campaign_id": "fca_3KlMnOpQrStUvWxYz01234",
"sequence_id": "fsq_5HgFdSaQwErTyUiOpLkJh",
"inbox_id": "fin_6ZxCvBnMaSdFgHjKlQwEr",
"occurred_at": "2026-09-26T09:30:00.000Z",
"message_id": "fms_8PoIuYtReWqAsDfGhJkLz",
"reply_kind": "positive"
}
}sequence.completed
Every email in a sequence went out and the prospect never replied.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "sequence.completed",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"campaign_id": "fca_3KlMnOpQrStUvWxYz01234",
"sequence_id": "fsq_5HgFdSaQwErTyUiOpLkJh",
"inbox_id": "fin_6ZxCvBnMaSdFgHjKlQwEr",
"occurred_at": "2026-09-26T09:30:00.000Z",
"outcome": "unanswered"
}
}Campaigns
Three of these tell you a campaign needs you.
campaign.pausedmeans the whole campaign stopped sending. Ifreasonisbounce_rate, too many emails bounced back, which usually means the list has bad addresses. If it'sspam_complaint, someone marked an email as spam. We pause first so your inbox keeps a good name. Readfix, do what it says, then resume the campaign. Ifreasonis null, you or a teammate paused it, and there's nothing to fix. Any otherreason, likenot_entitled, means the campaign is missing something it needs, such as a plan that can send or a connected inbox. Sort that out, then resume.campaign.inbox_droppedmeans one inbox the campaign sends from lost its connection. Only the emails from that inbox are on hold. The rest keep going. Reconnect the inbox in Settings and its emails pick up where they stopped.campaign.send_failedmeans one person couldn't be emailed, often because we have no address for them. Everyone else still gets their emails.
campaign.launched
A campaign started sending.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "campaign.launched",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"campaign_id": "fca_3KlMnOpQrStUvWxYz01234",
"occurred_at": "2026-09-26T09:30:00.000Z"
}
}campaign.paused
A campaign stopped sending. reason is bounce_rate or spam_complaint when we paused it to protect your inbox, and null when you paused it yourself. When we pause it, fix says in one sentence what to do. A bounce_rate pause also has sent and bounced, counted since our last pause. If the campaign can't carry on, for example your plan can't send or every inbox is gone, reason says that instead.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "campaign.paused",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"campaign_id": "fca_3KlMnOpQrStUvWxYz01234",
"occurred_at": "2026-09-26T09:30:00.000Z",
"reason": "bounce_rate",
"fix": "Too many emails bounced. Check the list for bad addresses, then resume.",
"sent": 40,
"bounced": 5
}
}campaign.inbox_dropped
One of a campaign's inboxes disconnected. Only the sequences sending from that inbox paused. The campaign keeps sending from its other inboxes, and the paused ones pick up again when you reconnect it.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "campaign.inbox_dropped",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"campaign_id": "fca_3KlMnOpQrStUvWxYz01234",
"occurred_at": "2026-09-26T09:30:00.000Z",
"inbox_id": "fin_6ZxCvBnMaSdFgHjKlQwEr",
"reason": "inbox_disconnected"
}
}campaign.resumed
A paused campaign started sending again.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "campaign.resumed",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"campaign_id": "fca_3KlMnOpQrStUvWxYz01234",
"occurred_at": "2026-09-26T09:30:00.000Z"
}
}campaign.stopped
Someone stopped a campaign for good.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "campaign.stopped",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"campaign_id": "fca_3KlMnOpQrStUvWxYz01234",
"occurred_at": "2026-09-26T09:30:00.000Z"
}
}campaign.finished
Every prospect in a campaign has finished their sequence.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "campaign.finished",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"campaign_id": "fca_3KlMnOpQrStUvWxYz01234",
"occurred_at": "2026-09-26T09:30:00.000Z"
}
}campaign.send_failed
A campaign couldn't send to one prospect. The rest of the campaign carries on.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "campaign.send_failed",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"campaign_id": "fca_3KlMnOpQrStUvWxYz01234",
"occurred_at": "2026-09-26T09:30:00.000Z",
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"sequence_id": "fsq_5HgFdSaQwErTyUiOpLkJh",
"reason": "no_email"
}
}Deals and partners
deal.agreed
You and a prospect agreed a deal. The terms are frozen at version; fetch the deal to read them.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "deal.agreed",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"deal_id": "fdl_2PqRsTuVwXyZ0123456789",
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"version": 2,
"agreed_by": "usr_1QaZxSwEdCvFrTgBnHyUj"
}
}affiliate.activated
An agreed deal became a live partner in AffiliateRail, with a commission rule on your program.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "affiliate.activated",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"deal_id": "fdl_2PqRsTuVwXyZ0123456789",
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"program_id": "prg_4Jk2mN8pQrS7tUvWxYz01B",
"partner_id": "part_4Jk2mN8pQrS7tUvWxYz01C",
"rule_id": "flw_7WxYz0123456789AbCdEg",
"linked_existing": false,
"terms_summary": "20% for 12 months"
}
}Inboxes
inbox.connected
A mailbox was connected for sending.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "inbox.connected",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"inbox_id": "fin_6ZxCvBnMaSdFgHjKlQwEr",
"occurred_at": "2026-09-26T09:30:00.000Z"
}
}inbox.disconnected
A mailbox disconnected. Its sequences and campaigns paused until you reconnect it.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "inbox.disconnected",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"inbox_id": "fin_6ZxCvBnMaSdFgHjKlQwEr",
"reason": "provider_revoked",
"paused_sequences": 12,
"paused_campaigns": 1,
"occurred_at": "2026-09-26T09:30:00.000Z"
}
}Team and partners
affiliate.assigned
A prospect got a new owner on your team, or lost one. assignee_member_id is null when nobody owns them now. Member ids match GET /team. by_member_id is null when the API or the system did it.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "affiliate.assigned",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"assignee_member_id": "tm_4RfVbGtYhNjUmIkOlP0q1W",
"previous_member_id": null,
"by_member_id": "tm_7YhNbGtRfVcDeXsWzAq2E"
}
}note.added
Someone on your team added a note to a prospect. The note's text isn't in the event. Read it with the prospect's notes endpoint in the API.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "note.added",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"note_id": "fcn_2WsXcDeRfVbGtYhNjUmKi",
"author_member_id": "tm_4RfVbGtYhNjUmIkOlP0q1W"
}
}affiliate.invited
Someone marked a prospect invited to your affiliate program.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "affiliate.invited",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"by_member_id": "tm_4RfVbGtYhNjUmIkOlP0q1W"
}
}affiliate.signed_up
An invited prospect signed up as your affiliate. source is manual when your team marked it, or affiliaterail when we saw them join your AffiliateRail program.
{
"id": "fev_1AbCdEfGhIjKlMnOpQrStU",
"type": "affiliate.signed_up",
"version": 1,
"created_at": "2026-09-26T09:30:00.000Z",
"livemode": true,
"data": {
"prospect_id": "fdi_9aBcDeFgHiJkLmNoPqRsT",
"brand_id": "fbr_4Jk2mN8pQrS7tUvWxYz01A",
"source": "manual"
}
}Was this page helpful?