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. 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/v1 answers 503 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. 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/token and then GET /api/v1/accounts?limit=8.

    Open Settings > API

  3. 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, its csk_ secret as client_secret, and the API base https://app.co-signal.com/api/v1 as resource. 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/v1

    The answer carries an access_token that 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 narrower scope does not narrow it.

  4. 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 or lead_ 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, call GET /api/v1/leads/resolve with the CRM reference as input.

  5. 5. Read the answer

    Every list answer has the same three parts: data holds the rows, page tells you whether more rows exist, and meta tells 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"
        ]
      }
    }

    sourceUpdatedAt is when your CRM last changed the record. observedAt is when Co-Signal last synced it. computedAt is when this specific answer was calculated. A fast response does not mean a fresh one; read all three.

    meta.limitations is a closed list of facts about the answer, such as order_is_stable_not_alphabetical, which means the order is stable but is not a name sort. A null sourceUpdatedAt that comes with no_crm_source_timestamp means 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_client means an unknown, revoked, suspended or expired key, a wrong secret or a disabled workspace, and these are deliberately indistinguishable.
  • 400 invalid_scope means you asked for a scope the key does not hold.
  • 400 invalid_target means resource is 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_type means 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_request means the request itself is malformed, including an unknown or repeated query parameter, which is refused rather than ignored.
  • 401 invalid_token means 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_scope means the key does not hold a scope this route needs.
  • 404 not_found means 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_limited means you went over a rate limit, so wait the number of seconds in Retry-After.
  • 503 feature_disabled means API access is not switched on, either everywhere or for your workspace, and the two are the same answer on purpose.
  • 503 temporarily_unavailable means something failed on the Co-Signal side, so retry after the seconds in Retry-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/accounts returns one row for each of your accounts with an empty partners array, 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_found as 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/resolve accepts a CRM reference (source and sourceRecordId together) or a company domain, 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/populations is one feed carrying both account and lead populations, told apart by resourceType.
  • 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 with chosen_field_values_not_served.
  • On GET /api/v1/contacts and GET /api/v1/opportunities, meta.sourceUpdatedAt is null with no_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 amount carries the sign your CRM gave it, so a negative amount is a valid credit, reversal or churn deal, and an account's openOpportunityValue is a sum of those amounts, so it can be negative too.
  • A lead's owner is 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.