API reference
Every endpoint a workspace key can reach
20 operations across 19 paths, generated from the server’s own route table: signals, accounts, listeners, pipeline, collections and exports. Each one carries the permission it needs and a request you can paste into a terminal.
Authenticate every request with Authorization: Bearer op_live_…. Mint a key in workspace settings and tick only the permissions it needs: a key always acts as a member, so a leaked one can read signals and move deals but can never create a listener, change billing, or mint another key.
Each example is the smallest request the document describes, with the real host and a placeholder secret. Replace anything in SHOUT_CASE and the braced segments of a path, and the call is one you can run.
1 of them are marked body not documented: those operations may take a JSON body that /openapi.json does not describe yet, so their example sends none and says so rather than presenting a command that may answer 400.
Operations are listed by path. A route the server mounts without a scope is absent from this page, because no key can call it — those are session-only, and documenting them would describe an interface you cannot reach.
GET /v1/accounts
List accounts
Requires accounts:read
Companies Open Pulse has found signals about, cursor-paginated. One row per company rather than per post, which is usually the shape a sales question wants.
Requires a plan that includes account intelligence; a workspace without it gets a 403 naming the plan rather than an empty list.
| Parameter | In | Description |
|---|---|---|
limit | query | Page size, 1–200. Defaults to 50. Values outside the range are clamped rather than rejected. |
cursor | query | nextCursor from the previous page. An unreadable cursor is ignored and the first page is returned, so check nextCursor for null rather than looping until a page comes back empty. |
owner | query | Only accounts assigned to this member, by Clerk user id. |
region | query | Only accounts whose region.label is exactly this. |
curl -X GET 'https://api.openpulse.cloud/v1/accounts' \
-H 'Authorization: Bearer op_live_YOUR_KEY'GET /v1/accounts/{key}
Get one account
Requires accounts:read
Everything known about one company: its signals, its owner and its region.
| Parameter | In | Description |
|---|---|---|
keyrequired | path | The namespaced account key, e.g. d:acme.com for a domain. Percent-encode it: the : is part of the key, not a separator. An aliasKeys entry resolves here too, so a key from before a merge still works. |
curl -X GET 'https://api.openpulse.cloud/v1/accounts/{key}' \
-H 'Authorization: Bearer op_live_YOUR_KEY'POST /v1/collections
Create a list
Requires collections:write
A named list to save signals into, shared across the workspace — a colleague who cannot see a list does the work twice, so createdBy is attribution rather than access control.
Counts against the plan's list cap. The workspace's saved collection does not: it is a system list created on first use, and charging for it would be charging for a star.
curl -X POST 'https://api.openpulse.cloud/v1/collections' \
-H 'Authorization: Bearer op_live_YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{
"name": "YOUR_NAME"
}'GET /v1/content/pushes/{id}/export.zip
Download a push's content as a zip
Requires export:read
Every piece written for one push, plus its assets, as a zip archive.
Unlike the CSV exports this has **no admin gate and no quota**: one push is already one thing somebody is looking at, so the narrowing rule has nothing to protect, and the pieces inside were paid for per target when they were generated. Charging a second time to read what you already own would be charging twice for one thing.
| Parameter | In | Description |
|---|---|---|
idrequired | path | The id of the resource. |
curl -X GET 'https://api.openpulse.cloud/v1/content/pushes/{id}/export.zip' \
-H 'Authorization: Bearer op_live_YOUR_KEY'GET /v1/exports/accounts.csv
Export accounts as CSV
Requires export:read
Streams the accounts matching the filters. A key's export is always filtered to what it may read: there is no unscoped dump, because a key is never an admin.
| Parameter | In | Description |
|---|---|---|
format | query | Column set. raw is every field under its own name and is the default; the crm- formats are the same values under the headers that importer expects, never different values. An unknown format answers 400. |
owner | query | Only accounts assigned to this member, by Clerk user id. |
region | query | Only accounts whose region.label is exactly this. |
curl -X GET 'https://api.openpulse.cloud/v1/exports/accounts.csv' \
-H 'Authorization: Bearer op_live_YOUR_KEY'GET /v1/exports/pipeline.csv
Export the pipeline as CSV
Requires export:read
Streams the deals matching the filters, with their stages and values.
| Parameter | In | Description |
|---|---|---|
format | query | Column set. raw is every field under its own name and is the default; the crm- formats are the same values under the headers that importer expects, never different values. An unknown format answers 400. |
stage | query | Only deals in this stage. |
open | query | true for open stages only. Ignored when stage is set. |
curl -X GET 'https://api.openpulse.cloud/v1/exports/pipeline.csv' \
-H 'Authorization: Bearer op_live_YOUR_KEY'GET /v1/exports/signals.csv
Export signals as CSV
Requires export:read
Streams the signals matching the filters. Counts against the workspace's export quota; the response is a stream, so read it rather than buffering it whole.
| Parameter | In | Description |
|---|---|---|
format | query | Column set. raw is every field under its own name and is the default; the crm- formats are the same values under the headers that importer expects, never different values. An unknown format answers 400. |
listenerId | query | Only this listener's signals. |
classification | query | Intent label; valid values depend on the listener's mode. |
maxAgeDays | query | Only signals seen within this many days. |
source | query | Platform the signal came from. |
theme | query | Only signals carrying this theme. |
q | query | Free-text search over the signal's title and summary. Matches whole words, with stemming: report finds reporting, repo does not. Truncated past 120 characters. |
curl -X GET 'https://api.openpulse.cloud/v1/exports/signals.csv' \
-H 'Authorization: Bearer op_live_YOUR_KEY'GET /v1/listeners
List listeners
Requires listeners:read
Every listener in the workspace, newest first, cursor-paginated. Each item carries its mode, its objective, its schedule and a count of the signals it has kept.
A key can read listeners and never change one: creating, editing and deleting a listener spends money on a recurring basis and stays with workspace admins.
| Parameter | In | Description |
|---|---|---|
limit | query | Page size, 1–200. Defaults to 50. Values outside the range are clamped rather than rejected. |
cursor | query | nextCursor from the previous page. An unreadable cursor is ignored and the first page is returned, so check nextCursor for null rather than looping until a page comes back empty. |
curl -X GET 'https://api.openpulse.cloud/v1/listeners' \
-H 'Authorization: Bearer op_live_YOUR_KEY'GET /v1/listeners/{id}
Get one listener
Requires listeners:read
One listener with its full configuration — keywords, exclusions, per-source queries, sensitivity and schedule — plus its current scheduler state.
An id belonging to another workspace is indistinguishable from one that does not exist: both answer 404.
| Parameter | In | Description |
|---|---|---|
idrequired | path | The id of the resource. |
curl -X GET 'https://api.openpulse.cloud/v1/listeners/{id}' \
-H 'Authorization: Bearer op_live_YOUR_KEY'POST /v1/listeners/analyse-website
Derive a listener plan from a website
Requires listeners:analyse
Crawls a website and returns a complete listener plan: keywords, exclusions, per-source queries, and a mode-specific profile — target roles, rival names and pain vocabulary, or place categories. Everything is editable before it runs.
This is the step every competing product replaces with an empty keyword box.
Rate limited to 5 calls an hour per key, and counted against the workspace's monthly allowance. An unreadable website answers 422 with website_unreadable rather than an error: the caller is asked to describe the business instead. Nothing is counted against the allowance unless the analysis succeeded.
To recover from a website_unreadable 422, resend the same request with description filled in and acceptLimited: true: the plan is then generated from the description instead of a crawl.
curl -X POST 'https://api.openpulse.cloud/v1/listeners/analyse-website' \
-H 'Authorization: Bearer op_live_YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{
"companyName": "YOUR_COMPANY_NAME",
"website": "https://example.com",
"objective": "find_customers",
"sources": [
"x"
]
}'POST /v1/notes
Write a note
Requires collections:write
Attaches a note to a signal, an account or a list.
**Workspace-visible**, deliberately: a colleague who cannot see that somebody already called them will call again. A note on a signal also notifies everyone already in that conversation, minus the author.
curl -X POST 'https://api.openpulse.cloud/v1/notes' \
-H 'Authorization: Bearer op_live_YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{
"subject": {
"type": "signal",
"id": "YOUR_ID"
},
"body": "YOUR_BODY"
}'GET /v1/pipeline
List deals
Requires pipeline:read
Every deal in the workspace's pipeline with its stage, cursor-paginated. The board shows all columns at once by fetching pages until they are exhausted; do the same rather than assuming one response is the whole pipeline.
| Parameter | In | Description |
|---|---|---|
limit | query | Page size, 1–200. Defaults to 50. Values outside the range are clamped rather than rejected. |
cursor | query | nextCursor from the previous page. An unreadable cursor is ignored and the first page is returned, so check nextCursor for null rather than looping until a page comes back empty. |
curl -X GET 'https://api.openpulse.cloud/v1/pipeline' \
-H 'Authorization: Bearer op_live_YOUR_KEY'POST /v1/pipeline
Add a deal
Requires pipeline:write
Opens one deal from the signal that prompted it. The card's signal* fields are copied from that signal here and never refreshed afterwards.
**Idempotent by signal.** A unique index refuses a second deal for the same signal, and this answers 200 with the existing one rather than an error — adding a lead twice is a double click, not a mistake worth a red banner. A genuinely new deal answers 201, which is how a caller tells the two apart.
curl -X POST 'https://api.openpulse.cloud/v1/pipeline' \
-H 'Authorization: Bearer op_live_YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{
"signalId": "YOUR_SIGNAL_ID"
}'PATCH /v1/pipeline/{id}
Update a deal
Requires pipeline:write
Moves a deal between stages, or edits its value, owner or contact. **Partial**: only the fields present are changed, and contact is merged field by field rather than replaced — sending {"contact": {"phone": "…"}} does not delete an email somebody else recorded.
Send null to clear ownerUserId or dealValueMinor, and an empty string to clear a contact field. At least one field is required: an empty body answers 400 rather than succeeding at nothing.
Moving the stage is what fires the pipeline.stage_changed webhook, sets closedAt on won or lost, and clears it again if the deal reopens.
| Parameter | In | Description |
|---|---|---|
idrequired | path | The id of the resource. |
curl -X PATCH 'https://api.openpulse.cloud/v1/pipeline/{id}' \
-H 'Authorization: Bearer op_live_YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{}'POST /v1/pipeline/bulk
Add several deals at once
Requires pipeline:write
One request rather than a loop, which is what an agent working from a list of signals should reach for.
**Partial success is the expected outcome and answers 200.** A selection made from a table routinely contains signals already on the board, so the response counts what happened to each group rather than failing the batch over a duplicate. Nothing here is transactional and nothing needs to be: every insert is independent, and the unique index makes a repeated request converge rather than accumulate.
curl -X POST 'https://api.openpulse.cloud/v1/pipeline/bulk' \
-H 'Authorization: Bearer op_live_YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{
"signalIds": [
"YOUR_SIGNAL_IDS"
]
}'GET /v1/pipeline/roi
Pipeline return over a window
Requires pipeline:read
What the pipeline sourced from Open Pulse signals is worth over the window: deals opened, deals won, and value. The answer to 'is this paying for itself'.
| Parameter | In | Description |
|---|---|---|
days | query | Window length in days. Bounds which deals *closed* inside it; openCount and weightedOpenMinor describe the board as it is now and ignore this. |
curl -X GET 'https://api.openpulse.cloud/v1/pipeline/roi' \
-H 'Authorization: Bearer op_live_YOUR_KEY'GET /v1/results
List signals
Requires signals:read
Kept signals across the workspace, newest first, cursor-paginated. A signal is one public post, job posting or review that a listener judged relevant, with its score and the reasoning behind it.
classification values differ per listener mode: filtering by one from the wrong mode returns an empty page rather than an error, which reads as 'nothing found' and is not. Pass listenerId to scope the filter to a listener whose mode you know.
| Parameter | In | Description |
|---|---|---|
listenerId | query | Only signals from this listener. |
limit | query | Page size, 1–200. Defaults to 50. Values outside the range are clamped rather than rejected. |
cursor | query | nextCursor from the previous page. An unreadable cursor is ignored and the first page is returned, so check nextCursor for null rather than looping until a page comes back empty. |
maxAgeDays | query | Only signals published within this many days. Defaults to the workspace's retention window, and cannot be used to read further back than it. |
classification | query | Intent label. **Valid values depend on the listener's mode**, and one from the wrong mode returns an empty page rather than an error — which reads as "nothing found" and is not. Pass listenerId too when you know the mode. |
source | query | Platform the signal came from. |
theme | query | Only signals carrying this theme. |
q | query | Free-text search over the signal's title and summary. Matches whole words, with stemming: report finds reporting, repo does not. Truncated past 120 characters. |
curl -X GET 'https://api.openpulse.cloud/v1/results' \
-H 'Authorization: Bearer op_live_YOUR_KEY'GET /v1/results/{id}
Get one signal
Requires signals:read
A single signal by id, regardless of how old it is or where it falls in the list's pagination. Use this to resolve a signal a link names; use GET /v1/results to browse.
| Parameter | In | Description |
|---|---|---|
idrequired | path | The signal's id. |
curl -X GET 'https://api.openpulse.cloud/v1/results/{id}' \
-H 'Authorization: Bearer op_live_YOUR_KEY'GET /v1/results/stats
Count signals in a window
Requires signals:read
Totals for the window, counted in the database rather than over a page, so the figures describe every signal rather than the ones that fit in one response. Use this instead of paging through /v1/results to answer 'how many'.
| Parameter | In | Description |
|---|---|---|
listenerId | query | Only count this listener's signals. |
maxAgeDays | query | Window in days. Defaults to the workspace's retention. |
source | query | Platform the signal came from. |
theme | query | Only signals carrying this theme. |
q | query | Free-text search over the signal's title and summary. Matches whole words, with stemming: report finds reporting, repo does not. Truncated past 120 characters. |
curl -X GET 'https://api.openpulse.cloud/v1/results/stats' \
-H 'Authorization: Bearer op_live_YOUR_KEY'POST /v1/signals/{id}/save
Save a signal to a list
Requires collections:write
Adds one signal to the workspace's saved list, creating that list on first use. **Takes no request body** — the signal is the path parameter, and there is nothing else to say.
Idempotent: saving a signal already saved is not an error and does not duplicate it. The response is 201 either way, carrying the existing membership when there was one, because "already saved" is not news anybody needs.
| Parameter | In | Description |
|---|---|---|
idrequired | path | The id of the resource. |
curl -X POST 'https://api.openpulse.cloud/v1/signals/{id}/save' \
-H 'Authorization: Bearer op_live_YOUR_KEY'The document declares no request body for this operation, so the example sends none. If the handler requires one, the call answers 400 until /openapi.json describes the shape.
Two rate limits, reported two ways. The burst limit on website analysis is per key and answers 429 with a Retry-After header. The monthly analysis allowance is per workspace and answers 429 with readyAt, used and allowed in the body, no header. Something unclear or wrong? Email support@openpulse.cloud.