Endpoints
The full reference for every Operelio API endpoint: trails and runs, leads, Do Not Contact, files and tools, CRM pushes, reports and Bridgeant.
By Operelio team · Updated November 1, 2026
On this page18
- 1.Base URL and how to read this page
- 2.Lists, pages and errors
- 3.Trails and runs
- 4.Leads
- 5.Do Not Contact
- 6.POST /uploads
- 7.POST /jobs
- 8.GET /jobs/:id
- 9.GET /files/:id/download
- 10.Batch endpoints (Agency)
- 11.POST /jobs/batch
- 12.GET /jobs/batch/:id
- 13.GET /jobs/batch/:id/download
- 14.Saved workflows
- 15.CRM
- 16.Reports
- 17.Bridgeant
- 18.Frequently asked questions
Base URL and how to read this page
Every request goes to the same base URL, over HTTPS, with your API key in the Authorization header:
https://operelio.com/api/v1 Authorization: Bearer op_your_api_key_here
Each endpoint below shows the request to send and the response you get back. The long ids are examples; use the ones your own calls return. For how to build the configJson each tool needs, see the Tools and configJson guide.
Lists, pages and errors
Most lists come 25 at a time, as { "data": [...], "nextCursor": "..." }. Ask for up to 100 with ?limit=. When nextCursor isn't null, send it back as ?cursor= for the next page. GET /trails, GET /crm/connections and GET /crm/owners return everything at once, with no nextCursor.
The endpoints for trails, leads, Do Not Contact, workflows, CRM and reports answer an error with { "error": "...", "code": "..." }: a sentence to show a person and a code to switch on. The file and job endpoints keep their own shapes. Every code is in Error handling.
Only writes, runs and Bridgeant messages count toward your monthly API calls. Reads don't: every GET, and POST /dnc/check. Writes need a read-and-write key, and the ones that change a trail, a routing team, your Do Not Contact list or a CRM need a read-and-write key made by an admin.
Trails and runs
Read your trails and their runs, see where each row went, pause and resume a trail, and change who is on a routing team. The Trails and leads guide shows each request and response.
| Endpoint | What it does |
|---|---|
| GET /trails | Every trail, its state, its CRM and how many leads are waiting or held. |
| GET /trails/:id | One trail, with its source, where it pushes and its routing. |
| GET /trails/:id/runs | Its runs, newest first. |
| GET /runs/:id | One run, with a page of where its rows went and why. |
| POST /trails/:id/pause | Pause a trail. Counts as a call. |
| POST /trails/:id/resume | Switch it back on, after the same checks as the button. Counts as a call. |
| GET /trails/:id/routing | Its rules, teams and who is next in each team. |
| PUT /trails/:id/routing/teams/:groupId/members | Change who is on a team. Counts as a call. |
Leads
Read the single leads your trails took in from a form, Typeform, a webhook or an email: the ones waiting for review, the ones held, or all of them, with each lead's score, owner and timeline.
| Endpoint | What it does |
|---|---|
| GET /leads | Leads by state (review, held or all), for every trail or one. |
| GET /leads/:id | One lead and its timeline: when it was routed and pushed. |
Do Not Contact
Search, add to and check your Do Not Contact list. The Do Not Contact guide shows how each value is matched.
| Endpoint | What it does |
|---|---|
| GET /dnc/entries | Search your list. |
| POST /dnc/entries | Add up to 500 entries at once. Counts as a call. |
| DELETE /dnc/entries/:id | Remove one entry. Counts as a call. |
| POST /dnc/check | Check up to 100 values. A read, so it doesn't count. |
POST /uploads
Upload one file so a job can run on it. Send it as multipart form data with the file in a field named "file". Accepts .xlsx, .xls, and .csv. Returns the file's id, which you pass as inputFileId when you create a job.
To feed a trail instead, add a folderId field with the id of a watched folder. The file lands in that folder exactly as if someone had dropped it there or emailed it in, it shows in Files, and the trail watching the folder runs on it. You do not create a job: the trail is the job. The folder id is the last part of the folder's URL in Files, and an id that is not one of your folders returns a 404 without storing anything.
curl -X POST https://operelio.com/api/v1/uploads \
-H "Authorization: Bearer op_your_api_key_here" \
-F "file=@contacts.csv"
# Response 200
{
"id": "8f3b7a10-2c4d-4e6a-9b1f-0d5c8e2a7f34",
"originalName": "contacts.csv",
"sizeBytes": 12345
}
# Into a watched folder, so the trail runs on it
curl -X POST https://operelio.com/api/v1/uploads \
-H "Authorization: Bearer op_your_api_key_here" \
-F "file=@contacts.csv" \
-F "folderId=2c1f5e88-7d3a-4b9e-a1c0-6f4d2e8b9a17"
# Response 200
{
"id": "5d2a9c41-8e7b-4f30-b6a2-1c9e7d3f5a08",
"originalName": "contacts.csv",
"sizeBytes": 12345,
"journeyId": "e7b1c3d9-4a52-4f6e-8d10-2b7c9e5a1f43",
"folderId": "2c1f5e88-7d3a-4b9e-a1c0-6f4d2e8b9a17"
}| Field | What it is |
|---|---|
| id | The file's id (a UUID). Pass this as inputFileId when you create a job, or in the fileIds array for a batch. |
| originalName | The filename you uploaded. |
| sizeBytes | The file size in bytes. |
| folderId (request, optional) | The id of a watched folder. Send it and the upload lands in that folder for its trail to run on; leave it out for a file you will run a job on yourself. |
| journeyId, folderId (response) | Returned only for a folder upload: the file's entry in Files and the folder it landed in. |
An upload is refused if the file type is wrong, the file is over your plan's size cap, or you have no jobs left this cycle. Other problems show up when the job runs. One request can be at most 4.5 MB, whatever your plan's cap; a bigger request gets a 413.
POST /jobs
Run one tool on one file. Send JSON with the file's id (inputFileId), the tool to run (toolType), and that tool's settings (configJson). Returns a job id with a status of "queued". The job runs in the background; poll it with GET /jobs/:id.
curl -X POST https://operelio.com/api/v1/jobs \
-H "Authorization: Bearer op_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"inputFileId": "8f3b7a10-2c4d-4e6a-9b1f-0d5c8e2a7f34",
"toolType": "deduplicate",
"configJson": { "columns": ["email"], "keepStrategy": "first" }
}'
# Response 200
{
"id": "3a9c1e57-6b8d-42f0-9a3e-7c1b5d0f2e88",
"toolType": "deduplicate",
"status": "queued",
"createdAt": "2026-07-23T10:00:00.000Z"
}| Send | What it is |
|---|---|
| inputFileId | The id from POST /uploads. Every file tool needs it. |
| toolType | Which tool to run, e.g. "deduplicate". See the Tools and configJson guide for the full list. |
| configJson | The tool's settings. The shape depends on the tool; capture it from the app (see the Tools guide). |
The response status is always "queued" at first. configJson is capped at 100 KB. Sending an unknown toolType returns 400 with the list of valid ones.
GET /jobs/:id
Check a job's status. Call this every couple of seconds until status is "completed" or "failed". While it runs, status moves queued → processing → completed (or failed). When it completes, outputFileId and outputFileName point to the result.
curl https://operelio.com/api/v1/jobs/3a9c1e57-6b8d-42f0-9a3e-7c1b5d0f2e88 \
-H "Authorization: Bearer op_your_api_key_here"
# Response 200
{
"id": "3a9c1e57-6b8d-42f0-9a3e-7c1b5d0f2e88",
"toolType": "deduplicate",
"status": "completed",
"outputFileId": "b2e60d94-1f7a-4c83-8d05-6a9e3b1c7f42",
"outputFileName": "contacts.csv",
"createdAt": "2026-07-23T10:00:00.000Z",
"startedAt": "2026-07-23T10:00:01.000Z",
"finishedAt": "2026-07-23T10:00:04.000Z",
"errorMessage": null
}| Field | What it is |
|---|---|
| status | One of: queued, processing, completed, failed. |
| outputFileId | The result file's id once the job completes. null until then. Pass it to GET /files/:id/download. |
| outputFileName | The result file's name once complete. null until then. |
| createdAt, startedAt, finishedAt | When the job was created, picked up, and finished. startedAt and finishedAt are null until each happens. |
| errorMessage | Why the job failed, when status is "failed". null otherwise. |
A job that failed still returns 200 here, with status "failed" and errorMessage set. That is different from a 4xx or 5xx, which means the request itself didn't reach the job. See Error handling.
GET /files/:id/download
Download a file by its id. Use the outputFileId from a completed job to get the result, or an input file's id to get back what you uploaded. The response is the file itself, not JSON.
curl -O -J https://operelio.com/api/v1/files/b2e60d94-1f7a-4c83-8d05-6a9e3b1c7f42/download \ -H "Authorization: Bearer op_your_api_key_here"
The -O -J flags tell curl to save the file under the name Operelio sends in the Content-Disposition header, instead of printing it to the screen. In your own code, read that header to name the file.
Batch endpoints (Agency)
When you have many files to run through the same tool with the same settings, batch does it in one request instead of one job per file. Batch is on the Agency plan. It supports six tools: Excel to CSV, CSV to Excel, Clean Headers, Data Health Check, Deduplicate, and Transpose.
One batch counts as a single API call against your rate limit, no matter how many files. An Agency batch takes up to 50 files and 500,000 rows across all of them.
POST /jobs/batch
Create a batch: one tool, one configJson, many files. Upload each file first with POST /uploads, then pass their ids in the fileIds array. Returns a batchId plus ready-made URLs to poll progress and download the combined result.
curl -X POST https://operelio.com/api/v1/jobs/batch \
-H "Authorization: Bearer op_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"fileIds": ["8f3b7a10-...", "1c2d3e4f-...", "9a8b7c6d-..."],
"toolType": "deduplicate",
"configJson": { "columns": ["email"], "keepStrategy": "first" }
}'
# Response 200
{
"batchId": "e7d9c1b2-3a45-4f60-8c17-2b9e0d5a3f18",
"jobIds": ["...", "...", "..."],
"pollUrl": "https://operelio.com/api/v1/jobs/batch/e7d9c1b2-3a45-4f60-8c17-2b9e0d5a3f18",
"downloadUrl": "https://operelio.com/api/v1/jobs/batch/e7d9c1b2-3a45-4f60-8c17-2b9e0d5a3f18/download"
}| Field | What it is |
|---|---|
| batchId | The batch's id. |
| jobIds | One job id per file, in upload order. Each file becomes its own job. |
| pollUrl | A full URL to poll the batch's progress. Use it as-is; you don't have to build it. |
| downloadUrl | A full URL to download every finished file as one ZIP. |
fileIds must be unique. A batch of N files needs N jobs left to start, and each file that completes counts as one job against your monthly quota. Files that fail don't count. The whole batch is one call against the rate limit.
GET /jobs/batch/:id
Poll a batch. Returns a progress summary and the status of every job in it. Poll the pollUrl from the create step every 5 to 10 seconds until completed plus failed equals total. A poll is a read, so it doesn't count toward your monthly API calls.
# Response 200
{
"batchId": "e7d9c1b2-3a45-4f60-8c17-2b9e0d5a3f18",
"toolType": "deduplicate",
"createdAt": "2026-07-23T10:00:00.000Z",
"progress": { "total": 3, "completed": 2, "failed": 0, "processing": 1 },
"jobs": [
{
"id": "...",
"status": "completed",
"inputFile": { "id": "...", "originalName": "list-a.csv" },
"outputFile": { "id": "...", "originalName": "list-a.csv" },
"errorMessage": null
}
]
}| Field | What it is |
|---|---|
| progress.total | How many files are in the batch. |
| progress.completed, failed, processing | Counts by state. Jobs still queued aren't counted in any of these, so early on the three can add up to less than total. |
| jobs[] | One entry per file, with its status, its input and output file, and an errorMessage if it failed. |
GET /jobs/batch/:id/download
Download every successful file in the batch as one ZIP. Call the downloadUrl once every file has finished. Called earlier, it returns a ZIP of only the files finished so far, or a 400 if none have finished. If some files failed, you still get a ZIP of the ones that worked; if every file failed, this returns 400 because there is nothing to download.
curl -O -J https://operelio.com/api/v1/jobs/batch/e7d9c1b2-3a45-4f60-8c17-2b9e0d5a3f18/download \ -H "Authorization: Bearer op_your_api_key_here" # Saves e.g. deduplicate_batch_2026-07-23_e7d9c1.zip
As with single-file downloads, -O -J saves the ZIP under the name in the Content-Disposition header. That name includes the tool, the date, and a short batch code, so two batches never overwrite each other.
Saved workflows
GET /workflows lists the workflows saved in the workspace, including ones shared with it. POST /workflows/run runs one on a file you uploaded, by its name or its id, and returns a job to poll with GET /jobs/:id, like any other job. It counts toward your monthly jobs and your monthly API calls.
Names are matched exactly, ignoring case. If two saved workflows share the name, the request returns 409 with code ambiguous_workflow_name and both ids, so you can run the one you mean by its id. A workflow whose last step is CRM Formatter can't run on the API: push the finished file with POST /jobs/:id/push instead.
curl -X POST https://operelio.com/api/v1/workflows/run \
-H "Authorization: Bearer op_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "workflow": "Weekly cleanup", "inputFileId": "8f3b7a10-2c4d-4e6a-9b1f-0d5c8e2a7f34" }'CRM
Push a finished file to a CRM you connected in the app. CRMs are connected in the app only, because connecting means signing in to the CRM in a browser.
POST /jobs/:id/push starts the push. It returns 200 when the push finished within the request, or 202 with status pushing while it carries on. It uses the workspace's connection for the job's CRM unless you send connectionId, and takes an optional ownerId (one CRM user for every record) or ownerColumn (the column that names each record's owner). GET /jobs/:id/push reports where it is and, once done, what it created and updated.
| Endpoint | What it does |
|---|---|
| GET /crm/connections | The CRMs you connected in the app, and whether each works now. |
| POST /jobs/:id/push | Push a finished file to your CRM. Counts as a call. |
| GET /jobs/:id/push | What the push created and updated. |
| GET /crm/owners | Your CRM's active users, for ownerId and routing teams. Send connectionId= when you have more than one CRM connected. |
| GET /mappings | Your saved column mappings. Read only: change them in the app. |
| GET /mappings/:id | One saved mapping, yours, a system template or your Agency library's. |
A push is refused with a code when it can't start: job_not_pushable, already_pushed, held, connection_inactive, provider_mismatch or crm_reconnect_needed. Error handling says what each means.
Reports
GET /analytics/:view returns the numbers on the Analytics tab for one view: trails, cleanup, verification, company_active, crm or operator (shown as Jobs in the app). Add days= for the period (7, 30, 90, 365 or all, and 30 when you leave it out) and, on the trails view only, trail= for one trail. Each view needs the plan it needs in the app.
GET /activity lists everything that ran in the workspace and who ran it, the same as Activity on Home. Filter it with q=, tool=, status=, who= and since=. Work done with a key shows the key's name.
Bridgeant
POST /bridgeant/ask sends Bridgeant one message as the person who made the key, and uses their Bridgeant messages as a chat turn in the app does. Send message, and optionally conversationId to carry on a conversation and fileId to ask about a file. It needs Starter or up and a read-and-write key whose maker is still in the workspace.
When Bridgeant offers to do something, pendingAction has the id to confirm, a kind (run_plan, run_trail, build_trail, save_trail or change_trail) and a one-line summary. Otherwise pendingAction is null. Its id isn't always the message's id, so confirm the one in pendingAction.
POST /bridgeant/actions/:id/confirm runs it, exactly as the card's button in the app would, and takes no body. A plan answers with ran and jobId, and a trail run with outcome and runId. A trail Bridgeant builds or saves is saved switched off, and a change is saved as the trail's draft: test it and put it live from the trail's page. Only a key made by an admin is offered the trail actions.
curl -X POST https://operelio.com/api/v1/bridgeant/ask \
-H "Authorization: Bearer op_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "message": "Get this file ready for HubSpot", "fileId": "8f3b7a10-2c4d-4e6a-9b1f-0d5c8e2a7f34" }'
# Response 200
{
"conversationId": "3c9e1f5a-7b2d-4e8a-9f6c-1a4d7b0e2c58",
"message": {
"id": "e4a7c2d9-5b1f-4c3e-8a6d-0f9b2e7c1a43",
"content": "I found duplicate rows and messy headers in leads.csv. Here's a plan to fix them."
},
"pendingAction": {
"id": "a1d3f6b8-2c4e-4a7f-9b0d-5e8c1f3a6b92",
"kind": "run_plan",
"summary": "Clean up leads.csv: Deduplicate, then Clean Headers (2 jobs)."
}
}An ask counts toward your monthly API calls only when it used a Bridgeant message, so a question about how Operelio works is free, even after the month's calls run out. A confirm counts once what it runs has started, or the trail is saved.
Related
Frequently asked questions
Are jobs synchronous or asynchronous?
Asynchronous. POST /jobs returns immediately with a job id; you poll GET /jobs/:id until status is "completed" or "failed". A 2-second poll interval is plenty.
What do the ids look like?
Every file, job, and batch id is a UUID, like 8f3b7a10-2c4d-4e6a-9b1f-0d5c8e2a7f34. You never make them up; you read them from one response and pass them to the next call.
How do I know what configJson a tool needs?
That is the one part that changes per tool. The Tools and configJson guide lists every toolType and shows how to capture the exact configJson from the app, so you copy a working shape rather than guess it.
Do API jobs count toward my monthly quota?
Yes. Every job that completes counts as one against your monthly job quota (1,000 on Pro, 3,000 on Agency), the same as running it in the app. Failed jobs and Apply Fixes runs don't count. A batch of N files needs N jobs left to start, and each file that completes uses one.
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.