Skip to content

Campaign Action Webhooks

A campaign action can send each reply it matches to a webhook: Zapier, Make, n8n, or an address of your own system. The request is JSON, signed as Standard Webhooks signs, and the same for every reply, so the tool on the other end can build its mapping from the first one.

Set it up

  1. On the campaign's Actions page, add an action and pick Send to a webhook.
  2. Paste the receiver's address. It must be a public https address; one on a private network, or with a user name or password in it, is refused. If the receiver wants a key, press Add a key and give the header's name and value; the value is stored encrypted and never shown again.
  3. Press Send test. A test sends a marked sample ("test": true) with realistic values in every field, so you can map the fields in your tool. Nothing is sent when an action or an address is saved.

Addresses belong to the organisation, and any campaign's actions can pick them. Only owners and admins add an address, change its key, see its signing secret, and remove an address no action uses (in the address list).

n8n

An n8n address with /webhook-test/ in its path only listens while the workflow editor waits for a test event. Use the production address (/webhook/) for the action.

The request

POST /your/path HTTP/1.1
content-type: application/json
user-agent: OpenProspect-Webhooks/1.0
webhook-id: 0198f2c4-6d1e-7c3a-9b1e-2f4a5d6c7e80
webhook-timestamp: 1790674867
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
  "id": "0198f2c4-6d1e-7c3a-9b1e-2f4a5d6c7e80",
  "type": "reply.interested",
  "timestamp": "2026-09-29T09:41:07Z",
  "api_version": "2026-10-01",
  "test": false,
  "data": {
    "classification": "interested",
    "classification_reason": "Asks for a call next week and names two dates.",
    "prospect_first_name": "Jana",
    "prospect_last_name": "Keller",
    "prospect_full_name": "Jana Keller",
    "prospect_email": "jana.keller@nordglas.example",
    "prospect_job_title": "Head of Operations",
    "company_name": "Nordglas GmbH",
    "company_website": "https://nordglas.example",
    "company_country": "DE",
    "campaign_id": "7c1d0f4e-2a3b-4c5d-8e9f-0a1b2c3d4e5f",
    "campaign_name": "Hohlglas Nord",
    "flow_name": "Glass makers",
    "thread_subject": "Re: Faster dispatch planning",
    "reply_text": "Thanks for reaching out. Could we talk on Tuesday or Wednesday?",
    "reply_text_truncated": false,
    "reply_from_email": "jana.keller@nordglas.example",
    "reply_received_at": "2026-09-29T09:40:12Z",
    "sent_from_email": "anna@openprospect.example",
    "app_url": "https://campaigns.openprospect.io/campaigns/7c1d0f4e-2a3b-4c5d-8e9f-0a1b2c3d4e5f/actions?reply=…"
  }
}
  • id is the delivery's id and the webhook-id header. It stays the same on every retry and when the delivery is sent again by hand, so a receiver that stores the ids it has seen drops a repeat. A test has a new id every time.
  • type is reply. and the reply's classification: interested, not_interested, referral, out_of_office, wrong_person, do_not_contact, unactionable.
  • data is one level deep and every key is always present. What is not known is the empty string; prospect and company fields are filled when the reply's address belongs to a prospect or company OpenProspect found for you.
  • reply_text is the reply as the email provider delivered it, cut at 10,000 characters; reply_text_truncated says whether it was cut.
  • Within one api_version, fields are never renamed or removed.

Check the signature

The signature is an HMAC-SHA256 over webhook-id, webhook-timestamp and the body, joined with dots, keyed with the secret's bytes after whsec_, in base64. Compare it with each v1, entry of webhook-signature, and refuse a timestamp more than five minutes away from your clock. Standard Webhooks libraries (standardwebhooks, svix) do this for you; in plain Python:

import base64
import hashlib
import hmac
import time


def verify(secret: str, headers: dict[str, str], body: bytes) -> None:
    """Raise unless the request was signed with the secret in the last five minutes."""
    message_id, timestamp = headers["webhook-id"], headers["webhook-timestamp"]
    if abs(time.time() - int(timestamp)) > 5 * 60:
        raise ValueError("the timestamp is more than five minutes away")
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{message_id}.{timestamp}.".encode() + body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    for candidate in headers["webhook-signature"].split(" "):
        version, _, signature = candidate.partition(",")
        if version == "v1" and hmac.compare_digest(signature, expected):
            return
    raise ValueError("no signature matches the secret")

Use the body exactly as it arrived; a parsed and re-serialised body does not verify.

After the secret is rotated, every request carries two signatures for 24 hours, the new secret's first, so you can switch the receiver to the new secret at any time in that day without missing a reply.

Answer

Answer with any 2xx within 15 seconds. The answer's first 2,000 characters are kept for the action's history, and no more than 64 KB of it is read.

Your answer What happens
2xx Delivered.
408, 429, 5xx, no answer within 15 seconds, address unreachable Tried again after about 1 minute, then with the wait doubling up to 4 hours, for about 12 hours in all; a Retry-After header sets a longer wait. Then the delivery fails.
3xx Fails at once. Redirects are not followed; a test shows where the address pointed.
401, 403 Fails at once. The receiver wants a key.
404 Fails at once. Nothing listens at the address.
410 Fails, and the address is paused.
400 with "Queue is full" (Make) Fails at once. The Make scenario's queue is full.
any other 4xx Fails at once. The receiver did not take the request.
an invalid certificate Fails at once.

The person who added an action hears about a failed delivery by email, at most once a day for each action and reason.

When an address is paused

An address is paused when it answers 410, or when every delivery to it failed for three days in a row (at least three deliveries). The person who added it is told by email, and its actions show it as paused.

While it is paused, nothing is sent; matching replies are kept for 30 days. To resume, an owner or admin presses Send test and resume on one of its actions, and a test that is delivered resumes the address. The page then offers to send the replies kept while it was paused. They are not sent by themselves.

Send again

OpenProspect's team can send a failed delivery again once its address is active. It goes with the same webhook-id as before.