- Prosyo Docs
- Integrations
- Custom triggers: start a campaign from any webhook
On this page
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. 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 or write a first-touch line (1 credit 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#
- 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.
- 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>. - Generate an API token in Integrations → Chrome extension & API → Generate API token. Copy it now, because it's shown once.
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)
- 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.
- HTTP Method:
- HTTP Request: add lead
- Method
POST, URLhttps://app.prosyo.com/api/v1/ext/leads - Authentication: Header Auth →
Authorization: Bearer YOUR_TOKEN - Body (JSON):
{ "listName": "Website form", "leads": [{ "fullName": "{{ $json.body.name }}", "email": "{{ $json.body.email }}", "companyName": "{{ $json.body.company }}", "linkedinUrl": "{{ $json.body.linkedin }}" }] }
- Method
- HTTP Request: enroll
- Method
POST, URLhttps://app.prosyo.com/api/v1/ext/campaigns - Same Header Auth
- Body (JSON):
{ "campaignId": "YOUR_CAMPAIGN_ID", "prospectIds": {{ JSON.stringify($json.data.prospectIds) }} }
- Method
- 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#
- Trigger: your app (Typeform, Webflow, HubSpot, Stripe, Calendly, or anything else), or Webhooks by Zapier → Catch Hook for a generic trigger URL.
- Action: Webhooks by Zapier → Custom Request (add lead)
- Method
POST, URLhttps://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
- Method
- Action: Webhooks by Zapier → Custom Request (enroll)
- Method
POST, URLhttps://app.prosyo.com/api/v1/ext/campaigns - Same headers
- Data:
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.
{ "campaignId": "YOUR_CAMPAIGN_ID", "prospectIds": ["{{data__prospectIds}}"] }
- Method
Option 3: Make#
- Webhooks → Custom webhook as the trigger. Copy the URL. That's your trigger URL.
- HTTP → Make a request to
/api/v1/ext/leads(Raw JSON body,Authorization: Bearerheader). - HTTP → Make a request to
/api/v1/ext/campaignswithcampaignIdandprospectIdsmapped from step 2. Turn on Parse response in step 2 so you can mapdata.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.
// 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:
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):
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 | 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 (delay 60–90 days) | Tag closed-lost |
| Job change (Clay, Common Room, UserGems) | Job changers | Job-change playbook | Enrich |
| New hire posted (hiring-signal tool) | Hiring signals | Hiring-signal playbook | — |
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 to build loops:
- Reply received → an n8n Switch → if the reply is positive,
POST /bulkwithaction: "stage"andstage: "interested", then create a CRM deal. - Stage changed to
not_interested→POST /bulkwithaction: "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.
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. Existing people are updated and don't count. |
| Duplicates | Matched by LinkedIn URL and email. The same person is never enrolled twice in one campaign. |
| Role | The token's user needs Member or higher to add leads and enroll |
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 | Already enrolled, Do not contact, or missing LinkedIn for a LinkedIn-only sequence | Expected. Prosyo protects people from double outreach. |
See the full API reference.
