Webhooks
NotiQ POSTs JSON events to your HTTPS URL when workspace activity matches a subscription.
CRUD is dashboard-only (/api-mcp) — there is no REST create/list/delete for webhooks.
Subscribable events
| Event | When | Reply-only fields populated? |
|---|---|---|
reply.received | Inbound Gmail reply on a campaign lead | Yes (messageText, subject, from, ids, timestamp, direction) |
lead.tagged | Lead tag set/changed | No (those fields are null) |
account.disconnected | Connected Gmail account disconnected | No — expect accountId (+ optional lead/campaign nulls) |
Only these three events are offered. LinkedIn-era events such as connection.accepted are not emitted.
Envelope
{
"id": "evt_<24 hex>",
"type": "reply.received",
"createdAt": "2026-04-01T12:00:00.000Z",
"data": { }
}
data fields
| Field | Notes |
|---|---|
campaignId | string | null |
leadId | string | null |
accountId | Gmail account id | null |
fullName | Lead name | null |
email | Lead email | null |
companyName | | null |
website | | null |
tag | Tag label | null |
linkedinUrl | Always null (Maps/email product) |
messageText | Reply body/snippet (truncated ~4000); mostly for replies |
subject | Inbound subject — reply only (else null) |
from | Inbound From — reply only |
messageId | Gmail message id — reply only |
conversationId | Gmail thread id — reply only |
messageTimestamp | ISO — reply only |
direction | "incoming" on replies; else null |
Signature
When you configure a signing secret on the webhook (length 8–256):
X-Webhook-Signature: <exact shared secret>
This is a static shared secret (header value equals the secret you stored), not HMAC. Convenient for n8n Header Auth. If no secret is set, the header is omitted.
To verify: compare the header value to your stored secret with a constant-time equality check.
User-Agent: Notiq-PublicAPI-Webhooks/1.0.
Delivery behavior
| Behavior | Detail |
|---|---|
| Method | POST JSON |
| Retries | Up to 3 attempts (backoff ~200 / 800 / 2000 ms) |
| Timeout | 10s per attempt |
| Redirects | Max 5; SSRF / private-IP blocked |
| Semantics | At-most-once (no durable queue) |
| Campaign filter | Empty campaignIds = all; else only listed campaigns |
Example: reply.received
{
"id": "evt_a1b2c3d4e5f678901234abcd",
"type": "reply.received",
"createdAt": "2026-04-01T12:00:00.000Z",
"data": {
"campaignId": "search-xyz",
"leadId": "lead_abc",
"accountId": "acc_abc",
"linkedinUrl": null,
"fullName": "Jane Doe",
"email": "jane@acme.com",
"companyName": "Acme Dental",
"website": "https://acme.com",
"messageText": "Thanks — happy to chat next week.",
"subject": "Re: Quick question",
"from": "Jane Doe <jane@acme.com>",
"tag": null,
"messageId": "18e…",
"conversationId": "18e…",
"messageTimestamp": "2026-04-01T11:59:50.000Z",
"direction": "incoming"
}
}