Trails and leads: runs, held leads and routing
Read your trails, their runs and every lead they took in over the Operelio API: where each lead went and why, what is held, and how to pause or resume a trail.
By Operelio team · Updated November 1, 2026
On this page8
What you can do
The Operelio API reads your trails, their runs and the leads they took in, so your own code can see what ran, where each lead went and why, and what is waiting for a person. It can also pause a trail, switch it back on, and change who is on a routing team.
Reads work with any key, read only or read and write, and don't count toward your monthly API calls. They are limited only by the 60 requests a minute each key can make. Pausing, resuming and changing a team are writes: they need a read-and-write key made by an admin, and each one counts as a call.
The API is on Pro and Agency. Every path below starts with https://operelio.com/api/v1, and every request carries your key in the Authorization header.
List your trails
GET /trails returns every trail in the workspace: its state, its CRM, and how many leads are waiting or held. GET /trails/:id returns one trail with the same fields plus where its files come from (its folder and schedule), where it pushes, and a summary of its routing.
curl https://operelio.com/api/v1/trails \
-H "Authorization: Bearer op_your_api_key_here"
# Response 200
{
"data": [
{
"id": "4f2a8c1e-7b3d-4e9a-a6c2-1d8f5b0e3a71",
"name": "Inbound leads",
"state": "on",
"crm": { "provider": "hubspot", "connected": true },
"waitingRuns": 0,
"waitingLeads": 3,
"heldLeads": 17,
"stop": null,
"activatedAt": "2026-10-02T09:12:44.000Z",
"liveVersion": 6
}
]
}| Field | What it is |
|---|---|
| state | on, off, needs_attention (Operelio stopped it and it needs a change) or never_live (built but never switched on). |
| crm.provider, crm.connected | The CRM the trail pushes to (hubspot, salesforce or pipedrive, or null when it has none), and whether that connection works now. |
| waitingRuns, waitingLeads, heldLeads | Runs held or stopped that need you, leads waiting for a yes or no, and leads held until something is fixed. |
| stop.code, stop.message | Why Operelio stopped the trail, as a code and a sentence. stop is null when it is running or you switched it off yourself. |
| activatedAt, liveVersion | When it was last switched on, and which saved version is live. |
Runs and where each row went
GET /trails/:id/runs lists a trail's runs, newest first: each run's status, whether it was a test run, when it started and settled, how many rows went each way, and why any were held. GET /runs/:id returns one run with its routed rows, a page at a time: the name of the owner each row went to, the rule that picked them, and why. Ask for up to 100 rows with limit, and send the routing's nextCursor back as cursor for the next page.
curl https://operelio.com/api/v1/runs/9b1e4d7a-2c5f-4a83-b0d6-7e3a1c9f5b28 \
-H "Authorization: Bearer op_your_api_key_here"
# Response 200 (shortened)
{
"id": "9b1e4d7a-2c5f-4a83-b0d6-7e3a1c9f5b28",
"trailId": "4f2a8c1e-7b3d-4e9a-a6c2-1d8f5b0e3a71",
"status": "completed",
"isTestRun": false,
"startedAt": "2026-10-30T08:00:03.000Z",
"settledAt": "2026-10-30T08:01:41.000Z",
"routing": {
"data": [
{ "row": 0, "outcome": "owner", "owner": { "name": "Maya Ortiz" }, "why": "Rule 1: Country is United Kingdom or Ireland", "rule": 0 },
{ "row": 1, "outcome": "owner", "owner": { "name": "Sam Patel" }, "why": "Otherwise", "rule": null }
],
"total": 2,
"nextCursor": null
}
}A hold reason is always a short, safe sentence, the same one the trail's page shows. Raw text from a file or a model never comes back in it.
Held leads and leads to review
GET /leads lists single leads, the ones a trail took in one at a time from a form, Typeform, a webhook or an email. Add state=review for leads waiting for a yes or no, state=held for leads held until something is fixed, or state=all for every lead from the last 90 days, and trail=<id> for one trail's leads only. A lead the Scorecard kept for a low score isn't waiting on anyone, so it shows only under state=all.
Each lead comes with its state, its score, why it is held (a code in heldReason and a sentence in heldDetail), and its route: the owner's id and name, the rule number and why. GET /leads/:id adds the lead's timeline: when it was routed, and when it was pushed, with the CRM and the owner.
curl -G https://operelio.com/api/v1/leads \
-d state=held \
-d trail=4f2a8c1e-7b3d-4e9a-a6c2-1d8f5b0e3a71 \
-H "Authorization: Bearer op_your_api_key_here"
# Response 200
{
"data": [
{
"id": "c81e5a3f-0d4b-4c92-8e7a-6b1f2d9c4e05",
"trailId": "4f2a8c1e-7b3d-4e9a-a6c2-1d8f5b0e3a71",
"state": "held",
"score": null,
"heldReason": "missing_field",
"heldDetail": "No email or name arrived. The form may have changed.",
"route": null,
"receivedAt": "2026-10-30T09:14:02.000Z"
}
],
"nextCursor": "WyIyMDI2LTEwLTMwVDA5OjE0OjAyLjAwMFoiLCJjODFlNWEzZi0wZDRiLTRjOTItOGU3YS02YjFmMmQ5YzRlMDUiXQ"
}A pushed lead's timeline names the CRM it reached, not the record's id in that CRM. The API doesn't return the lead's email, so look the person up in your CRM by the email you sent in, or open the lead in the app.
Pause and resume a trail
POST /trails/:id/pause pauses a trail. It takes no body. New files and leads wait in its folder until it is switched back on.
POST /trails/:id/resume switches it back on, after exactly the checks the Switch on button runs: your plan's limit on live trails, a passing test for a trail Bridgeant built, no draft changes waiting on a trail that has never been live, a folder, a CRM and the mapping it needs, and its schedule. Send { "arrivalsFrom": "new" } to run only on what arrives from now on, or "all" to run on what arrived while it was off as well.
curl -X POST https://operelio.com/api/v1/trails/4f2a8c1e-7b3d-4e9a-a6c2-1d8f5b0e3a71/resume \
-H "Authorization: Bearer op_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "arrivalsFrom": "new" }'
# Response 422 when a check fails
{
"error": "This trail watches no folder, so nothing can arrive. Pick one on the trigger tile. It can be built and test run like this, but it cannot go live until it has one.",
"code": "no_folder"
}| Response | What it means |
|---|---|
| 403, code plan_trail_cap | Your plan already has as many live trails as it allows. upgradeRequired is true when a higher plan allows more. |
| 409, code status_changed | Someone changed the trail's state at the same moment. Read the trail again, then decide. |
| 422, a check's own code | A switch-on check failed, such as no_folder, no_destination or mapping_missing. The error sentence says what to fix, the same words as the button. |
| 422, code draft_pending | A trail that has never been live has draft changes waiting. Put them live or discard them on the canvas first. |
| 422, code builder_setup_needed | A trail Bridgeant built hasn't finished its setup. step names what is left. |
| 422, code schedule_invalid | The trail's saved schedule can't be used. Set it again in the app. |
Routing rules and teams
GET /trails/:id/routing returns the trail's routing rules in order, its teams, each team's people and their weights, and who is next up in each team. GET /crm/owners lists your CRM's active users, the people a team can hold.
PUT /trails/:id/routing/teams/:groupId/members sets who is on one team. Send members, each with an ownerId from GET /crm/owners and an optional weight (how many leads in each turn they get), and, as ifVersion, the version GET /trails/:id/routing gave you, so a change someone made in between isn't lost. Every ownerId must be an active user in your CRM. The trail must be switched off, with no unpublished routing changes on its canvas. The rules themselves are changed in the app.
curl -X PUT https://operelio.com/api/v1/trails/4f2a8c1e-7b3d-4e9a-a6c2-1d8f5b0e3a71/routing/teams/g_emea/members \
-H "Authorization: Bearer op_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"members": [
{ "ownerId": "81234567", "weight": 2 },
{ "ownerId": "81234590" }
],
"ifVersion": 6
}'A team holds up to 50 people, each with a weight from 1 to 10, and a trail up to 50 rules. Leads are dealt round a team in turn, and its place carries over from one lead and one run to the next.
Pages of results
Lists come 25 at a time. Ask for up to 100 with limit. When there are more, the response has a nextCursor: send it back as cursor to get the next page. nextCursor is null on the last page. GET /trails isn't paged: it returns every trail at once. A run's routing rows page the same way, by their own nextCursor.
curl -G https://operelio.com/api/v1/trails/4f2a8c1e-7b3d-4e9a-a6c2-1d8f5b0e3a71/runs \ -d limit=100 \ -d cursor=WyIyMDI2LTEwLTI5VDA4OjAwOjAzLjAwMFoiLCI5YjFlNGQ3YS0yYzVmLTRhODMtYjBkNi03ZTNhMWM5ZjViMjgiXQ \ -H "Authorization: Bearer op_your_api_key_here"
Frequently asked questions
Can I see why a lead went to a rep through the API?
Yes. Every routed lead comes with the owner's id and name, and every routed row of a run with the owner's name, along with the rule that picked them, counted from 0, and the reason, like "Rule 1: Country is United Kingdom or Ireland" or "Otherwise". It is the same reason the lead's page shows beside Owner.
Does reading trails and leads use my monthly API calls?
No. Reads don't count toward the monthly calls. Each key can make 60 requests a minute. Pausing, resuming and changing a team count, one call each.
Can the API switch on a trail that the button can't?
No. Resume runs the same checks as the Switch on button, in the same order, and refuses with the same reason when one fails.
Connect Operelio to your code or your assistant
Start free and add Operelio to Claude or ChatGPT today. The API and webhooks come with Pro.