Get your first answer from the Co-Signal API
This guide takes you from nothing to a correct first answer from the Co-Signal API. It covers exactly what the API supports today: reading your own accounts, leads, contacts, opportunities, partners, populations and account overlaps.
API access is released one workspace at a time, and it is not switched on for a workspace until Co-Signal adds that workspace to the rollout.
1. Get enabled
Ask your Co-Signal contact to add your workspace to the API rollout. Until then, Settings > API tells you that API access isn't turned on for this workspace yet, and every call to
/api/v1answers503 feature_disabled. A workspace outside the rollout gets exactly the same answer as an API that is switched off everywhere, so that response only tells you access is not on for you yet.2. Create a key
An admin of your workspace opens Settings > API and chooses Create key. Name the key, choose what it can read (Accounts, Leads, Contacts, Opportunities, Partners, Populations or Overlaps) and choose an expiry, which defaults to No expiry.
Copy the secret as soon as it is shown: it starts with
csk_and it is shown once. If you lose it, rotate the key to get a new secret. Anyone holding the secret can read what the key can read, so keep it out of source code and shared documents.Settings > API can also run a test call: paste the secret and it runs the same two steps this guide describes,
POST /api/oauth/tokenand thenGET /api/v1/accounts?limit=8.3. Get a token
Exchange the key for an access token with the OAuth 2.0 client credentials grant. Send the key's ID as
client_id, itscsk_secret asclient_secret, and the API basehttps://app.co-signal.com/api/v1asresource. Settings > API shows the key's ID as Key ID (client_id), on the key's row and beside the secret when a key is created or rotated.Exchange your key for an access token
curl -X POST https://app.co-signal.com/api/oauth/token -d grant_type=client_credentials -d client_id=YOUR_KEY_ID -d client_secret=csk_example_not_a_real_secret -d resource=https://app.co-signal.com/api/v1The answer carries an
access_tokenthat is valid for 600 seconds, and there is no refresh token: when it expires, exchange the key again. The token carries every scope the key holds, and asking for a narrowerscopedoes not narrow it.4. Make your first call
Send the token as a bearer token to any read route, starting with your own accounts.
Read your own accounts
curl https://app.co-signal.com/api/v1/accounts -H "Authorization: Bearer YOUR_ACCESS_TOKEN"Every id you get back is an opaque public id with a resource prefix, such as
acct_for an account orlead_for a lead. Store these ids as your join key: they are stable, they belong to your workspace, and a CRM record id never appears in a response. To go the other way, from a CRM record to a Co-Signal id, callGET /api/v1/leads/resolvewith the CRM reference as input.5. Read the answer
Every list answer has the same three parts:
dataholds the rows,pagetells you whether more rows exist, andmetatells you how fresh the answer is and what it does not cover.What comes back
{ "data": [ { "id": "acct_example7Qm2", "name": "Example Industries", "domain": "example.com", "type": "customer", "openOpportunityCount": 2, "openOpportunityValue": 48000, "provenance": [ { "fieldKey": "name", "source": "own", "sharing": "disclosed", "sourceUpdatedAt": "2026-09-20T14:05:00.000Z", "observedAt": "2026-09-21T09:00:00.000Z" } ] } ], "page": { "nextCursor": "EXAMPLE_NEXT_CURSOR", "hasMore": true }, "meta": { "requestId": "00000000-0000-4000-8000-000000000000", "schemaVersion": "2026-09-20", "observedAt": "2026-09-21T09:00:00.000Z", "computedAt": "2026-09-21T09:00:00.120Z", "sourceUpdatedAt": "2026-09-20T14:05:00.000Z", "limitations": [ "order_is_stable_not_alphabetical" ] } }sourceUpdatedAtis when your CRM last changed the record.observedAtis when Co-Signal last synced it.computedAtis when this specific answer was calculated. A fast response does not mean a fresh one; read all three.meta.limitationsis a closed list of facts about the answer, such asorder_is_stable_not_alphabetical, which means the order is stable but is not a name sort. A nullsourceUpdatedAtthat comes withno_crm_source_timestampmeans no source timestamp exists for that data, not that the data is missing.
Pages and cursors
limit sets the page size: it defaults to 25, its maximum is 200, and a value outside 1 to 200 is refused with 400 invalid_request rather than quietly adjusted. When page.hasMore is true, pass page.nextCursor back as cursor to get the next page; when it is false, nextCursor is null and you have every row.
Fetch the next page
curl "https://app.co-signal.com/api/v1/accounts?limit=50&cursor=YOUR_NEXT_CURSOR" -H "Authorization: Bearer YOUR_ACCESS_TOKEN"A cursor is an opaque, readable bookmark tied to the query that produced it: it is not encrypted or signed, and it carries no permission, so every page is checked again from scratch. Using a cursor with a different query is refused with 400 cursor_query_mismatch, and a malformed cursor is refused with 400 invalid_request. If the row a cursor points at has since left the feed, the next page continues after it. Pages are not a snapshot, and no answer carries an exact total.
GET /api/v1/overlaps/accounts composes at most 25 accounts per request, so a larger limit returns at most 25 rows with results_capped_by_request_budget, and hasMore stays true while rows remain. GET /api/v1/leads/resolve returns at most 25 rows with hasMore false, because it has no cursor, and says so with results_capped_by_request_budget when more matched. A larger page size of up to 1000 is a target for a later release, once a stable snapshot exists behind the cursor.
Errors, step by step
The two steps answer in two different shapes, so check which step answered before you debug.
At the token step
POST /api/oauth/token answers in the OAuth 2.0 shape, a body with a single error field.
401 invalid_clientmeans an unknown, revoked, suspended or expired key, a wrong secret or a disabled workspace, and these are deliberately indistinguishable.400 invalid_scopemeans you asked for a scope the key does not hold.400 invalid_targetmeansresourceis not the API base, and it is also the answer while the API is switched off for everyone, because until then the token endpoint does not accept the API as a resource.400 unsupported_grant_typemeans a client credentials request named a resource other than the API while the API is switched off.
At the resource step
Every /api/v1 route answers in one envelope, a body with error and requestId, and the requestId is the handle to quote when you ask for help.
400 invalid_requestmeans the request itself is malformed, including an unknown or repeated query parameter, which is refused rather than ignored.401 invalid_tokenmeans the token is missing, expired or revoked, belongs to a key revoked after the token was issued, or was issued for the MCP server rather than the API.403 insufficient_scopemeans the key does not hold a scope this route needs.404 not_foundmeans the id does not name a record you can see, and an id you cannot see behaves exactly like one that was never issued.429 rate_limitedmeans you went over a rate limit, so wait the number of seconds inRetry-After.503 feature_disabledmeans API access is not switched on, either everywhere or for your workspace, and the two are the same answer on purpose.503 temporarily_unavailablemeans something failed on the Co-Signal side, so retry after the seconds inRetry-After.
Rate limits
One key can make 120 requests a minute and one workspace 600 a minute, counted in fixed 60 second windows. Over either limit the answer is 429 rate_limited with Retry-After in seconds until the window ends. The limit counts who is calling, never what was asked for.
Three things to know before you build
Channels are off by default for every partner.
A partner's data flows through this API only once that partner has switched the automation channel on for your partnership, and it is off by default for every partnership. So on day one
GET /api/v1/overlaps/accountsreturns one row for each of your accounts with an emptypartnersarray, until a data owner turns automation on. That empty array is the owners' current choice, not a broken feed.An id you can't see behaves exactly like an id that doesn't exist.
An id issued to another workspace, or one that names a record you may not see, answers the same
404 not_foundas an id that was never issued, with no distinguishing error.No CRM writes through this API yet.
No request to this API writes to your CRM in this release, and CRM updates stay in the Co-Signal app.
What is available today
- Reading your own accounts, leads, contacts, opportunities, partners, populations and account overlaps.
- An admin created key with the capabilities you choose, a test call, rotation and revocation, all at Settings > API.
What is not available yet
- Webhooks, bulk exports, usage reporting, delivery logs and replay, and audit logs are not part of this release.
- Knowledge search is built but cannot be reached yet, because no key can be given the knowledge scope in this release.
- There is no lookup by email and no search by name:
GET /api/v1/leads/resolveaccepts a CRM reference (sourceandsourceRecordIdtogether) or a companydomain, and refuses a personal email domain. - Contacts and opportunities are top level feeds only, so there are no per account sub resources and no single account read other than
GET /api/v1/accounts/{accountId}/context. - Every key of your workspace sees every one of your partnerships, because there is no per partner key restriction yet.
Details worth knowing
GET /api/v1/populationsis one feed carrying both account and lead populations, told apart byresourceType.- Your chosen custom field values are served on
GET /api/v1/leads/{leadId}only: a contact carries no chosen field, and chosen opportunity values are stored but not served, which the opportunities feed states withchosen_field_values_not_served. - On
GET /api/v1/contactsandGET /api/v1/opportunities,meta.sourceUpdatedAtis null withno_crm_source_timestamp. - A lead, contact or deal whose stored name is blank is served with a stated placeholder name, and its provenance marks that name as
absent. - An opportunity
amountcarries the sign your CRM gave it, so a negative amount is a valid credit, reversal or churn deal, and an account'sopenOpportunityValueis a sum of those amounts, so it can be negative too. - A lead's
owneris omitted when the lead has no owner, and on your own reads nothing is withheld, so an omitted owner means there is none. - Length limits in the API reference are response size guards, not validation rules to rely on.
- Route names are Co-Signal's own, and this API does not promise wire compatibility with any other vendor's API.