---
title: "Custom triggers: start a campaign from any webhook"
description: "Turn any form, CRM, Stripe or Calendly event into LinkedIn outreach: add the lead and enroll them in a Prosyo campaign with n8n, Zapier, Make or code."
url: https://prosyo.com/docs/integrations/custom-triggers/
section: "Integrations"
updated: 2026-11-06
---

# Custom triggers: start a campaign from any webhook

A **custom trigger** starts Prosyo outreach when something happens in another app. For example:

- someone fills in your **website form** → they're added to a list and enrolled in a LinkedIn sequence,
- a **trial signup** in Stripe → the founder sends a personal LinkedIn touch,
- a **Calendly no-show** → a gentle follow-up sequence starts,
- a **HubSpot deal** moves to closed-lost → a revival campaign starts in 90 days.

Webhooks already let Prosyo *send* events out. See [Signed webhooks](https://prosyo.com/docs/integrations/webhooks.md). Custom triggers work the other way: **any app → Prosyo**.

---

## How it works

Every custom trigger follows the same three steps:

```
Your app ──webhook──▶ Trigger URL (n8n / Zapier / Make / your code)
                          │
                          ├─ 1. POST /api/v1/ext/leads      → add or update the person on a list
                          ├─ 2. POST /api/v1/ext/campaigns  → enroll them in a campaign
                          └─ 3. (optional) POST /api/v1/ext/enrich or /bulk
```

| Step | Endpoint | Result |
|---|---|---|
| 1. Add the lead | `POST /api/v1/ext/leads` | Returns `prospectIds` for new **and** existing people |
| 2. Enroll | `POST /api/v1/ext/campaigns` | Adds them to a campaign. A running campaign starts sending inside working hours. |
| 3. Optional | `POST /api/v1/ext/enrich` | Research the person (5 credits each) or write a first-touch line (2 credits each) |
| 3. Optional | `POST /api/v1/ext/bulk` | Tag, change stage, or add to another list |

Everything still follows your campaign's **daily limits, working hours, delay between actions and stop-on-reply**. A trigger can't make Prosyo send faster than your settings allow.

## Before you start

1. **Create the campaign** you want people to enter and **launch** it. If it's still a draft, people are enrolled but nothing sends until you launch.
2. **Get the campaign ID.** Call `GET /api/v1/ext/campaigns` (below), or open the campaign in Prosyo and copy the ID from the address bar: `app.prosyo.com/campaigns/<campaign-id>`.
3. **Generate an API token** in **Integrations → Chrome extension & API → Generate API token**. Copy it now, because it's shown once. Only owners and admins can generate one. If your workspace already has a token and you saved it, use it. Otherwise click **Rotate token**: it replaces every token, so paste the new one into your Chrome extension and every other tool that used the old one.

> **Important:** Treat the token like a password. Store it in your automation tool's credentials, not in plain text, and never put it in website code that visitors can see.

---

## Option 1: n8n (recommended for full control)

**Workflow:** Webhook → HTTP Request (add lead) → HTTP Request (enroll)

1. **Webhook** node
   - HTTP Method: `POST`
   - Path: `prosyo-trigger`
   - Respond: Immediately
   - Copy the **Production URL**. This is your custom trigger URL. Paste it into your form tool, CRM or app as the webhook destination.
2. **HTTP Request: add lead**
   - Method `POST`, URL `https://app.prosyo.com/api/v1/ext/leads`
   - Authentication: Header Auth → `Authorization: Bearer YOUR_TOKEN`
   - Body (JSON):
     ```json
     {
       "listName": "Website form",
       "leads": [{
         "fullName": "{{ $json.body.name }}",
         "email": "{{ $json.body.email }}",
         "companyName": "{{ $json.body.company }}",
         "linkedinUrl": "{{ $json.body.linkedin }}"
       }]
     }
     ```
3. **HTTP Request: enroll**
   - Method `POST`, URL `https://app.prosyo.com/api/v1/ext/campaigns`
   - Same Header Auth
   - Body (JSON):
     ```json
     {
       "campaignId": "YOUR_CAMPAIGN_ID",
       "prospectIds": {{ JSON.stringify($json.data.prospectIds) }}
     }
     ```
4. **Activate** the workflow.

Add an **If** node between steps 2 and 3 to enroll only good-fit leads, for example when the company size is over 20 or the email domain isn't Gmail.

## Option 2: Zapier

1. **Trigger:** your app (Typeform, Webflow, HubSpot, Stripe, Calendly, or anything else), or **Webhooks by Zapier → Catch Hook** for a generic trigger URL.
2. **Action: Webhooks by Zapier → Custom Request** (add lead)
   - Method `POST`, URL `https://app.prosyo.com/api/v1/ext/leads`
   - Headers: `Authorization: Bearer YOUR_TOKEN`, `Content-Type: application/json`
   - Data: the lead JSON above, with fields inserted from the trigger
3. **Action: Webhooks by Zapier → Custom Request** (enroll)
   - Method `POST`, URL `https://app.prosyo.com/api/v1/ext/campaigns`
   - Same headers
   - Data:
     ```json
     { "campaignId": "YOUR_CAMPAIGN_ID", "prospectIds": ["{{data__prospectIds}}"] }
     ```
     Insert **Data Prospect Ids** from step 2. If Zapier returns it as a comma-separated list, add a **Formatter → Utilities → Line-item to text** step, or use a **Code by Zapier** step to build the JSON array.

## Option 3: Make

1. **Webhooks → Custom webhook** as the trigger. Copy the URL. That's your trigger URL.
2. **HTTP → Make a request** to `/api/v1/ext/leads` (Raw JSON body, `Authorization: Bearer` header).
3. **HTTP → Make a request** to `/api/v1/ext/campaigns` with `campaignId` and `prospectIds` mapped from step 2. Turn on **Parse response** in step 2 so you can map `data.prospectIds`.

## Option 4: your own code (a trigger URL you host)

Deploy this small function to Vercel, Netlify, Cloudflare Workers or any Node server. Its URL becomes your custom trigger. Set `PROSYO_TOKEN`, `PROSYO_CAMPAIGN_ID` and `TRIGGER_SECRET` as environment variables.

```js
// POST /api/prosyo-trigger?key=TRIGGER_SECRET
export default async function handler(req, res) {
  if (req.method !== "POST" || req.query.key !== process.env.TRIGGER_SECRET) {
    return res.status(401).json({ ok: false });
  }
  const { name, email, company, linkedin, list = "Custom trigger" } = req.body || {};
  const headers = {
    Authorization: `Bearer ${process.env.PROSYO_TOKEN}`,
    "Content-Type": "application/json",
  };
  const lead = { fullName: name, companyName: company, linkedinUrl: linkedin };
  if (email) lead.email = email; // never send an empty email string

  const added = await fetch("https://app.prosyo.com/api/v1/ext/leads", {
    method: "POST",
    headers,
    body: JSON.stringify({ listName: list, leads: [lead] }),
  }).then((r) => r.json());
  if (!added.ok) return res.status(502).json(added);

  const enrolled = await fetch("https://app.prosyo.com/api/v1/ext/campaigns", {
    method: "POST",
    headers,
    body: JSON.stringify({
      campaignId: process.env.PROSYO_CAMPAIGN_ID,
      prospectIds: added.data.prospectIds,
    }),
  }).then((r) => r.json());

  return res.status(200).json({ ok: true, added: added.data, enrolled: enrolled.data });
}
```

**cURL test:**

```bash
curl -X POST "https://your-app.vercel.app/api/prosyo-trigger?key=YOUR_TRIGGER_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"name":"Alex Rivera","email":"alex@acme.com","company":"Acme","linkedin":"https://www.linkedin.com/in/alexrivera"}'
```

**Python (Flask):**

```python
import os, requests
from flask import Flask, request, jsonify

app = Flask(__name__)
API = "https://app.prosyo.com/api/v1/ext"
H = {"Authorization": f"Bearer {os.environ['PROSYO_TOKEN']}"}

@app.post("/prosyo-trigger")
def trigger():
    if request.args.get("key") != os.environ["TRIGGER_SECRET"]:
        return jsonify(ok=False), 401
    b = request.get_json(force=True)
    lead = {k: v for k, v in {
        "fullName": b.get("name"), "email": b.get("email"),
        "companyName": b.get("company"), "linkedinUrl": b.get("linkedin"),
    }.items() if v}
    added = requests.post(f"{API}/leads", headers=H,
                          json={"listName": "Custom trigger", "leads": [lead]}).json()
    ids = added["data"]["prospectIds"]
    enrolled = requests.post(f"{API}/campaigns", headers=H,
                             json={"campaignId": os.environ["PROSYO_CAMPAIGN_ID"], "prospectIds": ids}).json()
    return jsonify(ok=True, added=added["data"], enrolled=enrolled["data"])
```

---

## Ready-made trigger recipes

| Trigger (your app) | List | Campaign to enroll in | Extra step |
|---|---|---|---|
| **Website demo form** (Webflow, WordPress, HubSpot Forms, Typeform) | Inbound – demo form | "Inbound: fast LinkedIn touch": visit → connect → message within 1 day | Enrich (`personalized_line`) |
| **Gated content download** (ebook, report) | Content – [asset name] | Soft 3-touch: connect → "hope the guide helps" → offer a call | Tag `content` |
| **Webinar registration** (Zoom, Livestorm, Luma) | Event – [name] | [Webinar playbook](https://prosyo.com/docs/playbooks/webinar-event-promotion.md) | Tag `registered` |
| **Free trial signup** (Stripe, Paddle, your app) | Trials – this month | Founder welcome on LinkedIn | Stage `interested` |
| **Calendly / Cal.com no-show** | No-shows | 2-touch reschedule sequence | Stage `meeting` → `interested` |
| **HubSpot deal closed-lost** | Closed-lost | [Revival playbook](https://prosyo.com/docs/playbooks/closed-lost-revival.md) (delay 60–90 days) | Tag `closed-lost` |
| **Job change** (Clay, Common Room, UserGems) | Job changers | [Job-change playbook](https://prosyo.com/docs/playbooks/job-change-outreach.md) | Enrich |
| **New hire posted** (hiring-signal tool) | Hiring signals | [Hiring-signal playbook](https://prosyo.com/docs/playbooks/hiring-signal-outreach.md) | — |
| **Slack slash command** (`/prosyo add <linkedin url>`) | Team referrals | Your main ABM campaign | — |
| **Google Sheets new row** | Sheet – [name] | Any campaign | — |

## Chain triggers with Prosyo events

Combine custom triggers with [outgoing webhooks](https://prosyo.com/docs/integrations/webhooks.md) to build loops:

- **Reply received** → an n8n Switch → if the reply is positive, `POST /bulk` with `action: "stage"` and `stage: "interested"`, then create a CRM deal.
- **Stage changed to `not_interested`** → `POST /bulk` with `action: "add_to_list"` to a *Nurture – 6 months* list → a scheduled n8n job enrolls that list in a nurture campaign later.
- **Prospect created** from a form → wait 10 minutes → `POST /enrich` → enroll once enrichment is done.

> **Tip:** Not ready to start outreach yet? Send the event to Intent instead with `POST /signals` (for example a `signup` or `form_submit`). Intent adds it to the person's score, and your Intent playbooks can take it from there. See the [API reference](https://prosyo.com/docs/integrations/api-reference.md#post-signals).

## Rules and limits

| Rule | Value |
|---|---|
| Leads per `/leads` request | Up to 100 |
| Prospects per `/campaigns` enroll request | Up to 100 |
| Rate limit, `/leads` | 60 requests / minute / user |
| Rate limit, `/campaigns` enroll | 30 requests / minute / user |
| Each lead needs | A name, a LinkedIn URL or an email |
| Empty values | Leave the field out or send `null`. Don't send `""` for `email`. |
| Import quota | New people count toward monthly imports. People matched by their LinkedIn profile are updated and don't count. A lead with no LinkedIn URL is always added as a new person, even if its email is already in Prosyo, and is then saved without that email. |
| Duplicates | Leads are matched to people already in Prosyo by their LinkedIn profile, so send `linkedinUrl` whenever you have it. The same person is never enrolled twice in one campaign. |
| Role | A token acts as the owner or admin who generated it, with their current role. If that person's role is lowered to Viewer, the token can only read. |

## Troubleshooting

| Response | Meaning | Fix |
|---|---|---|
| `401` | Token missing, wrong, rotated or revoked | Generate a new token |
| `404` "That list is not in this workspace" | Wrong `listId` | Use `listName`, or get IDs from `GET /boards` |
| `422` "No usable leads" | Every lead was empty | Send at least a name, email or LinkedIn URL |
| `422` validation error on `email` | Empty or invalid email | Leave `email` out when you don't have one |
| `429` | Rate limit | Batch leads (up to 100) or add a short delay |
| Enrolled but nothing sends | Campaign is a draft or paused, or it's outside working hours | Launch or resume the campaign, and check working hours |
| `skipped` > 0 when enrolling | The person is already in that campaign, is marked Do not contact, or isn't visible to the token's user | Expected. Prosyo protects people from double outreach. |
| `403` with `plan_limit` | Everyone in the request was new, and the monthly import limit is reached | People already in Prosyo still update. Wait for next month or upgrade. If only some people fit, the request succeeds and `quotaBlocked` shows how many weren't added. |

See the full [API reference](https://prosyo.com/docs/integrations/api-reference.md).
