API reference

Signal API

Signal identifies the companies and people visiting your site. This API gives you that data programmatically — the organisations that visited ranked by intent, the people identified, where they arrived from, and what Signal currently recommends.

Read-only — no endpoint changes your workspaceVersion 0.1.0

Every endpoint, parameter, limit and response shape below is read from the running service at build time. Nothing on this page is maintained by hand, so it cannot describe an endpoint that does not exist or omit one that does.

This page is the endpoint reference. The developer guide, workflows and case studies live alongside the service they describe.

Quickstart

Create a key in Exports & API, then make your first request. The key is shown once, on creation.

curl
curl "https://app.signal.geysera.com/signal-api/v1/accounts" \
  -H "Authorization: Bearer sk_sig_..."
Base URL https://app.signal.geysera.com/signal-api/v1, as the service reports it in its own spec. We do not hardcode it here.

Authentication

Header
Authorization: Bearer sk_sig_...
Authorizationheaderrequired
Bearer sk_sig_... (required)
X-Correlation-IDheaderoptional
optional; generated when absent, echoed on every response
X-Idempotency-Keyheaderoptional
optional; honoured on POST /audiences and POST /audiences/{key}/runs — a retry with the same key replays the first answer instead of creating or spending twice. The other audience writes are idempotent by design.

401 means the key is missing, malformed, unknown or revoked. 403 means the key is valid but lacks the scope for that endpoint. The two are deliberately distinct: a 403 means your credential is fine and re-issuing it will not help.

Scopes

readscope
The /signal-api/v1 REST endpoints: accounts, visitors, attribution, recommendations.
copilotscope
Ask questions in natural language at /signal-api/v1/copilot/ask. READ-ONLY: a key reaches the copilot's read tools and never the ones that change the workspace, whatever it is asked.
audiencesscope
/signal-api/v1/audiences: create, edit and delete audiences, review prospects, estimate runs and export accepted people (JSON, or CSV with ?format=csv). Spends nothing.
enrichscope
Start a paid enrichment run (POST /signal-api/v1/audiences/{key}/runs) or change daily lookups. SPENDS enrichment credits, within the workspace's caps. Grant it only to a key that should spend.

A key is scoped to one workspace. Results respect that workspace's plan caps, exactly as the dashboard does. Scopes are fixed when the key is created: to change reach, create a new key and revoke the old one.

GET/accounts

List companies, one row per email domain. Consumer mailboxes share one "Personal email" row.

Query parameters

pageintegeroptional1–1000, default 1
Page number, from 1. Capped at 1000, so `page x page_size` bounds what offset pagination can reach: 50,000 rows at the default page size and 200,000 at the maximum. For a full export use `page_size=200`; to go beyond 200,000, narrow the query with a filter. Asking for page 1001 returns 422 rather than an empty page, so a walk cannot mistake the ceiling for the end of the data.
page_sizeintegeroptional1–200, default 50
Rows per page, 1-200. The default of 50 is sized for a screen; a full export should ask for 200, which also multiplies how far the 1000-page ceiling reaches.
classificationlead | customer | excluded or nulloptional

Response fields

accountsarray of PublicAccountrequired
totalintegerrequired
pageintegerrequired
page_sizeintegerrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl "https://app.signal.geysera.com/signal-api/v1/accounts" \
  -H "Authorization: Bearer sk_sig_..."
Response
{
  "accounts": [
    {
      "company_domain": "string",
      "company_name": "string",
      "intent_score": 0,
      "visitor_count": 0,
      "visit_count": 0,
      "classification": "string",
      "first_seen_at": "2026-09-11T14:23:07Z",
      "last_seen_at": "2026-09-11T14:23:07Z",
      "is_locked": false
    }
  ],
  "total": 0,
  "page": 0,
  "page_size": 0
}
GET/attribution

Visits, identifications and revenue by channel and campaign for a date range.

The nightly attribution rollup, served as-is.

Deliberately reads the cached rollup only and never falls back to a live scan, unlike the dashboard endpoint. A live recompute scans up to 200k visitor rows; behind an API key that is a trivially repeatable way to exhaust the database, and an integrator polling on a cron would do it without meaning to. computed_at tells the caller how fresh the answer is so staleness is visible rather than hidden.

Query parameters

daysintegeroptional1–365, default 30
Trailing window in days. Attribution is a nightly rollup computed for a fixed set of windows, so any other value returns `has_data: false` with `available_windows` listing the ones that exist — a self-correcting answer rather than an error. That is the commonest cause of `has_data: false`, and it is NOT the same as having no attribution: when the window exists but nothing was captured, `available_windows` is absent.

Response fields

window_daysintegerrequired
The window this rollup covers, in days.
has_databooleanrequired
False when no rollup exists for this window. The rollup fields are then `null` — PRESENT in the response with a null value, not omitted — and `available_windows` lists the windows that do have one. Branch on this flag: do not read a null or a zero as 'no attribution', and do not test for a missing key.
computed_attimestamp or nulloptional
When the rollup was last computed.
available_windowsarray of integer or nulloptional
The windows that DO have a rollup, so a caller can retry with one that exists. Populated only when `has_data` is false; `null` otherwise — the key is always present.
totalsAttributionWindowTotals or nulloptional
funnelarray of AttributionFunnelStage or nulloptional
Visitors -> Identified -> High intent. Nested by construction.
channelsarray of AttributionChannel or nulloptional
sourcesarray of AttributionSource or nulloptional
Where visitors came FROM. Answers where to spend.
campaignsarray of AttributionCampaign or nulloptional
landing_pagesarray of AttributionLandingPage or nulloptional
Pages visitors arrived ON. Answers what to write — a different question from `sources`, and not a substitute for it.
first_vs_lastarray of AttributionFirstVsLast or nulloptional
trendarray of AttributionTrendPoint or nulloptional
Dense over the window, so real gaps stay visible.
landing_channelarray of AttributionLandingChannel or nulloptional
revenueAttributionRevenue or nulloptional
Orders joined to a first touch. Read its `coverage_pct` first.
tenant_scopeinteger or nulloptional
How many tenants' data this rollup combines. 2 where a Signal workspace reads orders through a paired commerce tenant, 1 otherwise. Reported so a figure that spans both is not mistaken for one workspace's.

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl "https://app.signal.geysera.com/signal-api/v1/attribution" \
  -H "Authorization: Bearer sk_sig_..."
Response
{
  "window_days": 0,
  "has_data": false,
  "computed_at": "2026-09-11T14:23:07Z",
  "available_windows": [
    0
  ],
  "totals": {
    "visitors": 0,
    "visitors_total": 0,
    "inferred_touches": 0,
    "touch_coverage_pct": 0,
    "identified": 0,
    "identify_rate": 0,
    "high_intent": 0,
    "high_intent_rate": 0,
    "with_click_id": 0,
    "channels_active": 0,
    "median_hours_to_identify": 0,
    "multi_touch": 0,
    "multi_touch_rate": 0,
    "intent_threshold": 0,
    "truncated": false
  },
  "funnel": [
    {
      "stage": "string",
      "count": 0,
      "pct": 0
    }
  ],
  "channels": [
    {
      "channel": "string",
      "visitors": 0,
      "identified": null,
      "identify_rate": null,
      "high_intent": null,
      "avg_intent": null,
      "share": null
    }
  ],
  "sources": [
    {
      "source_medium": "string",
      "channel": null,
      "visitors": 0,
      "identified": null,
      "identify_rate": null,
      "high_intent": null,
      "avg_intent": null
    }
  ],
  "campaigns": [
    {
      "campaign": "string",
      "channel": null,
      "visitors": 0,
      "identified": null,
      "identify_rate": null,
      "high_intent": null,
      "avg_intent": null
    }
  ],
  "landing_pages": [
    {
      "path": "string",
      "visitors": 0,
      "identified": null,
      "identify_rate": null,
      "high_intent": null,
      "avg_intent": null
    }
  ],
  "first_vs_last": [
    {
      "channel": "string",
      "first_touch": 0,
      "last_touch": 0,
      "delta": 0,
      "role": null
    }
  ],
  "trend": [
    {
      "date": "string",
      "total": 0,
      "by_channel": null
    }
  ],
  "landing_channel": [
    {
      "path": "string",
      "channel": "string",
      "visitors": 0,
      "identified": null,
      "identify_rate": null
    }
  ],
  "revenue": {
    "orders": 0,
    "revenue": 0,
    "currency": "string",
    "coverage_pct": 0,
    "attributed_orders": 0,
    "attributed_revenue": 0,
    "unattributed_revenue": 0,
    "revenue_per_identified_visitor": 0,
    "channels": [],
    "products": []
  },
  "tenant_scope": 0
}
GET/audiences

Audiences defined for this workspace

Query parameters

No parameters.

Response fields

audiencesarray of PublicAudienceDefinitionrequired
store_rolesarray of PublicStoreRolerequired
The roles available for `confirmed_roles`, with their sizes.
readinessPublicAudienceReadinessrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl "https://app.signal.geysera.com/signal-api/v1/audiences" \
  -H "Authorization: Bearer sk_sig_..."
Response
{
  "audiences": [
    {
      "key": "string",
      "name": "string",
      "title_terms": [
        "string"
      ],
      "employer_terms": [
        "string"
      ],
      "page_patterns": [
        "string"
      ],
      "confirmed_roles": [
        "string"
      ],
      "industry_terms": [
        "string"
      ],
      "job_functions": [
        "string"
      ]
    }
  ],
  "store_roles": [
    {
      "role": "string",
      "customers": 0,
      "label": "string"
    }
  ],
  "readiness": {
    "ready": false,
    "missing": [
      {
        "need": "string",
        "message": "string",
        "href": "string",
        "label": "string"
      }
    ]
  }
}
POST/audiences

Create an audience from its name

The key is derived from the name (lower-case, `_` for spaces), so a caller never has to invent one. Refuses a name whose key already exists — edit that audience with PUT /audiences/{key} instead. Reports what the definition matches, like PUT.

Query parameters

X-Idempotency-Keystring or nulloptional

Status codes: 201 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 409 An audience with that name already exists, or this workspace cannot have one yet · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl -X POST "https://app.signal.geysera.com/signal-api/v1/audiences" \
  -H "Authorization: Bearer sk_sig_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"string"}'
GET/audiences/{key}

One audience's definition

Query parameters

keystringrequired

Response fields

keystringrequired
Stable identifier, derived from the name on creation.
namestringrequired
title_termsarray of stringrequired
employer_termsarray of stringrequired
page_patternsarray of stringrequired
SQL LIKE patterns against the page path. Empty matches nobody.
confirmed_rolesarray of stringrequired
Roles that count as confirmed members: the store's own, Geysera's (`geysera:`), or a purchase (`product:<id>`: bought that product, ever).
industry_termsarray of stringrequired
job_functionsarray of stringrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 This workspace has no audience with that key · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl "https://app.signal.geysera.com/signal-api/v1/audiences/{key}" \
  -H "Authorization: Bearer sk_sig_..."
Response
{
  "key": "string",
  "name": "string",
  "title_terms": [
    "string"
  ],
  "employer_terms": [
    "string"
  ],
  "page_patterns": [
    "string"
  ],
  "confirmed_roles": [
    "string"
  ],
  "industry_terms": [
    "string"
  ],
  "job_functions": [
    "string"
  ]
}
PUT/audiences/{key}

Create or replace an audience definition

Saves the definition and reports what it matches in this workspace's own data, so a term or page pattern that matches NOTHING is said at once rather than discovered as an empty list weeks later. Creating one is refused (409) where an audience cannot work yet — no store with members, or no enrichment on the plan — with what to fix; editing one that exists is not.

Query parameters

keystringrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 409 This workspace cannot have an audience yet · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl -X PUT "https://app.signal.geysera.com/signal-api/v1/audiences/{key}" \
  -H "Authorization: Bearer sk_sig_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"string"}'
DELETE/audiences/{key}

Delete an audience

Kept, not erased: the team's decisions and the run history stay (the stored classifications are removed — they describe a definition that is gone), and creating an audience with the same name brings it back (its calibration then reads as out of date, because the definition was re-made). Deleting also turns off its daily lookups, unsubscribes everyone from its daily email and pauses its sync rules, so nothing spends or sends for an audience that no longer exists. Refused while a run is in progress — cancel it first.

Query parameters

keystringrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 This workspace has no audience with that key · 409 A run for this audience is in progress · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl -X DELETE "https://app.signal.geysera.com/signal-api/v1/audiences/{key}" \
  -H "Authorization: Bearer sk_sig_..."
GET/audiences/{key}/acceptance

The spec's acceptance tests, on this workspace's data

Each check reports its bar, what it measured and over how many, and passes, fails, or says `pass: null` when the data cannot decide (the profile rate needs its whole sample looked up: start a run with `mode: acceptance`). Reads stored data and spends nothing; the made-up-address probe is POST /{key}/acceptance/made-up-addresses.

Query parameters

keystringrequired

Response fields

audiencestringrequired
The audience key these checks were run against.
passedintegerrequired
failedintegerrequired
undecidedintegerrequired
Criteria that could not be decided either way.
checksarray of PublicAcceptanceCheckrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 This workspace has no audience with that key · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl "https://app.signal.geysera.com/signal-api/v1/audiences/{key}/acceptance" \
  -H "Authorization: Bearer sk_sig_..."
Response
{
  "audience": "string",
  "passed": 0,
  "failed": 0,
  "undecided": 0,
  "checks": [
    {
      "check": "string",
      "label": "string",
      "bar": "string",
      "measured": "string",
      "pass": false,
      "detail": "string"
    }
  ]
}
POST/audiences/{key}/acceptance/made-up-addresses

Look up 20 invented addresses; none should return a profile

The spec's first acceptance test, live against the enrichment provider. Costs credits only if the provider wrongly returns a profile. Each lookup is checked against the spending limits and recorded in the credit ledger; no profile is stored. A key needs the `enrich` scope.

Query parameters

keystringrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 This workspace has no audience with that key · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl -X POST "https://app.signal.geysera.com/signal-api/v1/audiences/{key}/acceptance/made-up-addresses" \
  -H "Authorization: Bearer sk_sig_..."
GET/audiences/{key}/account-watch

Confirmed members to call: late reorders, and back on the site without buying

The spec's Account Watch, over the last `days`. `late`: members whose time since their last order is longer than any gap they have had between two orders, who became late in the window. `browsing_without_buying`: members the pixel identified viewing pages in the window, with no order since their first view in it. Orders are the store's own, attributed to the account (customer id first); `audience_pages` lists only pages matching the audience's patterns. Most members are never identified on the site, so the second list is short by nature.

Query parameters

keystringrequired
daysintegeroptional1–365, default 90
The window, in days.
limitintegeroptional1–200, default 50
Max people per list.

Response fields

audiencestringrequired
daysintegerrequired
The window these figures cover.
membersintegerrequired
known_accountsintegerrequired
orders_synced_throughtimestamprequired
How far this store's order history is known to be whole. Everything here is measured up to that point, not up to now.
latePublicLateMembersrequired
browsing_without_buyingPublicBrowsingWithoutBuyingrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 This workspace has no audience with that key · 409 This audience names no store role, so it has no confirmed members to watch · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl "https://app.signal.geysera.com/signal-api/v1/audiences/{key}/account-watch" \
  -H "Authorization: Bearer sk_sig_..."
Response
{
  "audience": "string",
  "days": 0,
  "members": 0,
  "known_accounts": 0,
  "orders_synced_through": "2026-09-11T14:23:07Z",
  "late": {
    "count": 0,
    "accounts": [
      {
        "account": "string",
        "name": null,
        "company": null,
        "orders": 0,
        "revenue": 0,
        "first_order_at": null,
        "last_order_at": null,
        "days_since_last_order": 0,
        "longest_gap_days": null,
        "late_since": null,
        "visitor_id": null
      }
    ]
  },
  "browsing_without_buying": {
    "count": 0,
    "people": [
      {
        "account": "string",
        "visitor_id": null,
        "name": null,
        "company": null,
        "orders": 0,
        "last_order_at": null,
        "page_views": 0,
        "first_view_in_window": "2026-09-11T14:23:07Z",
        "last_view": "2026-09-11T14:23:07Z",
        "audience_pages": []
      }
    ]
  }
}
PUT/audiences/{key}/automation

Turn daily lookups of an audience's new readers on or off

Kept apart from the definition on purpose: replacing the definition (PUT /{key}) must never switch spending on or off as a side effect.

Spends money, so a key needs the `enrich` scope.

Query parameters

keystringrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 This workspace has no audience with that key · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl -X PUT "https://app.signal.geysera.com/signal-api/v1/audiences/{key}/automation" \
  -H "Authorization: Bearer sk_sig_..." \
  -H "Content-Type: application/json" \
  -d '{"auto_enrich":false}'
GET/audiences/{key}/digest

The audience's daily email: who gets it and what was sent

Every morning, after the day's refresh, each subscriber receives the people accepted into the audience since the previous email — by the model, with its reason, or by a teammate — and the likely people still waiting for a decision. Readers nothing is known about are only counted. Only workspace members can subscribe, each for themselves. Nothing is sent before the audience is calibrated, to a workspace whose email is switched off, or on a day with nobody accepted or to review; `recent` says which happened each day. A day that was not sent for a reason other than "nobody new" keeps its people for the next email.

Query parameters

keystringrequired

Response fields

audiencestringrequired
subscribedboolean or nulloptional
Whether the caller is subscribed. null for an API key: a digest goes to a person, and a key is not one.
subscribersintegerrequired
workspace_email_suppressedbooleanrequired
True stops every send for this workspace, whatever the subscriptions say.
email_only_toarray of string or nulloptional
When set, the only addresses mail may reach. null means no restriction.
outputsPublicOutputsGaterequired
Whether this audience's outputs are switched on, and why not if not. One definition for every output: the page, the export, sync-rule creation and the hourly push all read this. There is no pass mark — the gate asks only that the CURRENT definition has been measured.
next_includes_people_sincetimestamp or nulloptional
Where the next digest's window starts.
recentarray of PublicDigestSendrequired
Newest first, at most 7.

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 This workspace has no audience with that key · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl "https://app.signal.geysera.com/signal-api/v1/audiences/{key}/digest" \
  -H "Authorization: Bearer sk_sig_..."
Response
{
  "audience": "string",
  "subscribed": false,
  "subscribers": 0,
  "workspace_email_suppressed": false,
  "email_only_to": [
    "string"
  ],
  "outputs": {
    "enabled": false,
    "reason": "string"
  },
  "next_includes_people_since": "2026-09-11T14:23:07Z",
  "recent": [
    {
      "sent_on": "string",
      "status": "string",
      "accepted": 0,
      "likely": 0,
      "candidate": 0,
      "recipients": 0,
      "delivered": 0,
      "detail": "string"
    }
  ]
}
PUT/audiences/{key}/digest

Subscribe yourself to the audience's daily email, or stop

For the signed-in person only — the email goes to their own address on this workspace's team. An API key cannot subscribe: it has no mailbox, and a prospect list must not be sent to an address typed in.

Query parameters

keystringrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope, or the caller is a key rather than a signed-in person (the daily email goes to a person) · 404 This workspace has no audience with that key · 409 Your account on this workspace has no email address to send to · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl -X PUT "https://app.signal.geysera.com/signal-api/v1/audiences/{key}/digest" \
  -H "Authorization: Bearer sk_sig_..." \
  -H "Content-Type: application/json" \
  -d '{"subscribed":false}'
GET/audiences/{key}/overview

The whole pipeline for one audience, stage by stage

One read for the pipeline: people who read the audience's pages (identified), how many have a professional profile and a company record (profiled), how many are likely or candidates and how many are already known customers (classified), the team's decisions (reviewed), the latest calibration, and runs in progress. Every count comes with what it is a count of.

Query parameters

keystringrequired

Response fields

audiencePublicAudienceDefinitionrequired
An audience as the workspace defined it. Every term list may be empty, and an empty `page_patterns` matches NOBODY rather than everybody — the definition is fail-closed.
stagesPublicAudienceStagesrequired
The funnel from reader to reviewed decision, stage by stage. Each stage counts a different population, so the figures are not a single series that narrows: read each against its own `of`/`of_what`.
calibrationPublicAudienceRun or nulloptional
The run that measured this definition, if one has.
calibration_stalebooleanrequired
True when the definition changed after it was measured.
outputsPublicOutputsGaterequired
Whether this audience's outputs are switched on, and why not if not. One definition for every output: the page, the export, sync-rule creation and the hourly push all read this. There is no pass mark — the gate asks only that the CURRENT definition has been measured.
active_runPublicAudienceRun or nulloptional
The run in progress, if any.
recent_runsarray of PublicAudienceRunrequired
Newest first, at most 5.
credits_this_monthintegerrequired
automationPublicAutomationStaterequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 This workspace has no audience with that key · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl "https://app.signal.geysera.com/signal-api/v1/audiences/{key}/overview" \
  -H "Authorization: Bearer sk_sig_..."
Response
{
  "audience": {
    "key": "string",
    "name": "string",
    "title_terms": [
      "string"
    ],
    "employer_terms": [
      "string"
    ],
    "page_patterns": [
      "string"
    ],
    "confirmed_roles": [
      "string"
    ],
    "industry_terms": [
      "string"
    ],
    "job_functions": [
      "string"
    ]
  },
  "stages": {
    "identified": {
      "readers": 0,
      "of_what": "string"
    },
    "profiled": {
      "profiled": 0,
      "with_company_record": 0,
      "of": 0
    },
    "classified": {
      "likely": 0,
      "likely_read_pages": 0,
      "candidate": 0,
      "confirmed": {
        "members": 0,
        "known_accounts": 0,
        "late_last_90_days": 0,
        "browsing_without_buying_last_90_days": 0
      }
    },
    "reviewed": {
      "accepted": 0,
      "rejected": 0,
      "reviewed": 0,
      "unreviewed": 0,
      "to_review": 0,
      "nothing_known": 0,
      "automatic": 0,
      "overruled": 0,
      "confirmed": 0
    }
  },
  "calibration": {
    "id": "string",
    "tenant_id": "string",
    "audience_key": "string",
    "mode": "string",
    "status": "string",
    "max_credits": 0,
    "credits_spent": 0,
    "counts": null,
    "result": null,
    "error": "string",
    "started_by": "string",
    "started_at": "2026-09-11T14:23:07Z",
    "finished_at": "2026-09-11T14:23:07Z"
  },
  "calibration_stale": false,
  "outputs": {
    "enabled": false,
    "reason": "string"
  },
  "active_run": {
    "id": "string",
    "tenant_id": "string",
    "audience_key": "string",
    "mode": "string",
    "status": "string",
    "max_credits": 0,
    "credits_spent": 0,
    "counts": null,
    "result": null,
    "error": "string",
    "started_by": "string",
    "started_at": "2026-09-11T14:23:07Z",
    "finished_at": "2026-09-11T14:23:07Z"
  },
  "recent_runs": [
    {
      "id": "string",
      "tenant_id": "string",
      "audience_key": "string",
      "mode": "string",
      "status": "string",
      "max_credits": 0,
      "credits_spent": 0,
      "counts": null,
      "result": null,
      "error": "string",
      "started_by": "string",
      "started_at": "2026-09-11T14:23:07Z",
      "finished_at": "2026-09-11T14:23:07Z"
    }
  ],
  "credits_this_month": 0,
  "automation": {
    "auto_enrich": false,
    "monthly_credit_budget": 0
  }
}
GET/audiences/{key}/prospects

People who look like this audience, with the evidence

`likely`: the person's title or employer matches the audience's terms. `candidate`: they read the audience's pages, but nothing known about them matches.

Evidence is limited to professional fields and the audience's own pages. Members of `confirmed_roles` (confirmed members), staff, GPC opt-outs and the suppression list never appear. `at_known_account` marks someone whose business email domain belongs to an account already in the excluded segment — a colleague at a known customer.

Query parameters

keystringrequired
tierlikely | candidate or nulloptional
One tier only. Omit for both.
include_reviewedbooleanoptionaldefault false
Include people already accepted or rejected.
limitintegeroptional1–200, default 50
Max rows per tier.
offsetintegeroptional0–100000, default 0
Skip this many rows per tier (paging).
sortname | email | title | employer | page_views | last_page_view | last_order or nulloptional
Sort each tier by this, across all its rows (not just the page). Omit for the default: colleagues at known accounts last, then most recently active.
directionasc | descoptionaldefault desc
With `sort`. Empty values always sort last.
sincetimestamp or nulloptional
Only people who entered their level at or after this time (ISO 8601) — the daily list of new prospects. The level's start is stored by a daily refresh and after every run, so someone new today appears once the next refresh has seen them. People already in the audience when its history began (`classification_history_starts_at`) are never new.

Response fields

audiencestringrequired
audience_namestringrequired
classification_history_starts_attimestamp or nulloptional
Before this, `classified_at` is unknown rather than absent.
likely_countintegerrequired
From the query, not the length of `likely`.
likely_viewed_audience_pagesintegerrequired
likely_matched_on_profile_onlyintegerrequired
Matched on their profile without viewing an audience page.
candidate_countintegerrequired
likelyarray of PublicProspectrequired
Title or employer matches the audience.
candidatearray of PublicProspectrequired
Read the audience's pages, but nothing known about them matches.
include_reviewedbooleanrequired
sortPublicProspectSortrequired
caveatsarray of stringrequired
What these figures do and do not say. Render them with the numbers.

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 This workspace has no audience with that key · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl "https://app.signal.geysera.com/signal-api/v1/audiences/{key}/prospects" \
  -H "Authorization: Bearer sk_sig_..."
Response
{
  "audience": "string",
  "audience_name": "string",
  "classification_history_starts_at": "2026-09-11T14:23:07Z",
  "likely_count": 0,
  "likely_viewed_audience_pages": 0,
  "likely_matched_on_profile_only": 0,
  "candidate_count": 0,
  "likely": [
    {
      "visitor_id": "string",
      "email": "string",
      "name": "string",
      "title": "string",
      "employer": "string",
      "linkedin_url": "string",
      "matched_on": [
        "string"
      ],
      "evidence_sources": {
        "title": "string",
        "employer": "string",
        "job_history": "string",
        "education": "string",
        "industry": "string",
        "job_function": "string",
        "model_read": "string",
        "audience_pages": "string"
      },
      "industry": "string",
      "job_function": "string",
      "headline": "string",
      "job_history": "string",
      "education": "string",
      "model_read": {
        "decision": null,
        "reason": null,
        "fields": null
      },
      "audience_pages": [
        "string"
      ],
      "audience_page_views": 0,
      "last_audience_page_view": "2026-09-11T14:23:07Z",
      "has_ordered": false,
      "last_order_at": "2026-09-11T14:23:07Z",
      "business_email": "string",
      "business_email_source": "string",
      "business_email_confidence": "string",
      "at_known_account": false,
      "review": "string",
      "classified_at": "2026-09-11T14:23:07Z"
    }
  ],
  "candidate": [
    {
      "visitor_id": "string",
      "email": "string",
      "name": "string",
      "title": "string",
      "employer": "string",
      "linkedin_url": "string",
      "matched_on": [
        "string"
      ],
      "evidence_sources": {
        "title": "string",
        "employer": "string",
        "job_history": "string",
        "education": "string",
        "industry": "string",
        "job_function": "string",
        "model_read": "string",
        "audience_pages": "string"
      },
      "industry": "string",
      "job_function": "string",
      "headline": "string",
      "job_history": "string",
      "education": "string",
      "model_read": {
        "decision": null,
        "reason": null,
        "fields": null
      },
      "audience_pages": [
        "string"
      ],
      "audience_page_views": 0,
      "last_audience_page_view": "2026-09-11T14:23:07Z",
      "has_ordered": false,
      "last_order_at": "2026-09-11T14:23:07Z",
      "business_email": "string",
      "business_email_source": "string",
      "business_email_confidence": "string",
      "at_known_account": false,
      "review": "string",
      "classified_at": "2026-09-11T14:23:07Z"
    }
  ],
  "include_reviewed": false,
  "sort": {
    "by": "string",
    "direction": "string"
  },
  "caveats": [
    "string"
  ]
}
GET/audiences/{key}/reviews

People accepted or rejected, newest first

Every decision, with each person's professional fields as they stand now. `automatic` is true when the model decided, from its reading of the person's professional profile (`reason`); it follows the model's latest reading. A decision the team made is kept even if the definition later stops matching the person: it is a human judgement, and neither the rules nor the model overrule it.

`format=csv` answers with `text/csv`, not JSON.

Query parameters

keystringrequired
decisionaccepted | rejected or nulloptional
Only this decision. Omit for both.
limitintegeroptional1–500, default 100
offsetintegeroptional0–100000, default 0
sortdecided_at | name | email | title | employer | last_orderoptionaldefault decided_at
Sort by this, across all rows.
directionasc | descoptionaldefault desc
Empty values always sort last.
formatjson | csvoptionaldefault json
csv: the whole list as a spreadsheet (limit and offset ignored) — the export. Same columns as the JSON.

Response fields

audiencestringrequired
totalintegerrequired
All matching decisions, not the length of `reviews`.
sortPublicReviewSortrequired
reviewsarray of PublicReviewrequired

Status codes: 200 The decisions, as JSON — or the whole list as a spreadsheet when `format=csv` · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 This workspace has no audience with that key · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl "https://app.signal.geysera.com/signal-api/v1/audiences/{key}/reviews" \
  -H "Authorization: Bearer sk_sig_..."
Response
{
  "audience": "string",
  "total": 0,
  "sort": {
    "by": "string",
    "direction": "string"
  },
  "reviews": [
    {
      "visitor_id": "string",
      "decision": "string",
      "decided_at": "2026-09-11T14:23:07Z",
      "automatic": false,
      "reason": "string",
      "email": "string",
      "name": "string",
      "title": "string",
      "employer": "string",
      "industry": "string",
      "linkedin_url": "string",
      "esp": {
        "state": null,
        "label": null,
        "lists": null
      },
      "last_order_at": "2026-09-11T14:23:07Z",
      "business_email": "string",
      "business_email_source": "string",
      "business_email_confidence": "string"
    }
  ]
}
POST/audiences/{key}/reviews

Record the team's decision on one prospect

Idempotent: the latest decision for a person stands. Identified by email, the way a reader names a person, rather than by an internal id.

Query parameters

keystringrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 No audience with that key, or no identified visitor with that email · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl -X POST "https://app.signal.geysera.com/signal-api/v1/audiences/{key}/reviews" \
  -H "Authorization: Bearer sk_sig_..." \
  -H "Content-Type: application/json" \
  -d '{"email":"string","decision":"accepted"}'
POST/audiences/{key}/reviews/clear

Withdraw the team's decision on one person

The person returns to the prospect list as undecided. For a decision made by mistake; a changed mind is a new decision (POST /reviews). A decision the model made is made again at the next refresh — overrule it with POST /reviews instead.

Query parameters

keystringrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 No audience with that key, or no decision recorded for that email · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl -X POST "https://app.signal.geysera.com/signal-api/v1/audiences/{key}/reviews/clear" \
  -H "Authorization: Bearer sk_sig_..." \
  -H "Content-Type: application/json" \
  -d '{"email":"string"}'
POST/audiences/{key}/runs

Look up the people in an audience, within a credit limit

Spends enrichment credits. Runs the pipeline — professional profile, then company record — over the audience's people, stopping before any lookup that would cross `max_credits`. Nobody is looked up twice; suppressed people and opt-outs are skipped; the workspace's credit caps and this audience's monthly budget (set with its daily lookups) still apply. Estimate first with POST /{key}/runs/estimate.

A key needs the `enrich` scope; `audiences` alone can never spend.

Query parameters

keystringrequired
X-Idempotency-Keystring or nulloptional

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 This workspace has no audience with that key · 409 A run for this audience is already in progress · 422 A parameter failed validation · 429 Rate limit exceeded for this key · 503 The run could not be started; nothing was spent

Request
curl -X POST "https://app.signal.geysera.com/signal-api/v1/audiences/{key}/runs" \
  -H "Authorization: Bearer sk_sig_..." \
  -H "Content-Type: application/json" \
  -d '{"mode":"prospects","max_credits":0}'
GET/audiences/{key}/runs

Recent runs for an audience, newest first

Query parameters

keystringrequired

Response fields

runsarray of PublicAudienceRunrequired
Newest first, at most 20.

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 This workspace has no audience with that key · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl "https://app.signal.geysera.com/signal-api/v1/audiences/{key}/runs" \
  -H "Authorization: Bearer sk_sig_..."
Response
{
  "runs": [
    {
      "id": "string",
      "tenant_id": "string",
      "audience_key": "string",
      "mode": "string",
      "status": "string",
      "max_credits": 0,
      "credits_spent": 0,
      "counts": null,
      "result": null,
      "error": "string",
      "started_by": "string",
      "started_at": "2026-09-11T14:23:07Z",
      "finished_at": "2026-09-11T14:23:07Z"
    }
  ]
}
POST/audiences/{key}/runs/{run_id}/cancel

Stop a run; it makes no further paid lookups

Takes effect before the next paid lookup. Credits already spent stay spent, and what was found stays found. Also releases a run whose worker stopped, which would otherwise block every later run of this audience.

Query parameters

keystringrequired
run_idstringrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 No audience with that key, or no such run · 409 That run has already finished · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl -X POST "https://app.signal.geysera.com/signal-api/v1/audiences/{key}/runs/{run_id}/cancel" \
  -H "Authorization: Bearer sk_sig_..."
POST/audiences/{key}/runs/estimate

Who a run would look up, and the most it could spend

Spends nothing. Counts the people a run would cover and, of those, the ones not looked up yet — the only ones a run pays for — and the most that could cost in enrichment credits. A lookup that finds nobody is free, so a run usually spends less.

Query parameters

keystringrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 This workspace has no audience with that key · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl -X POST "https://app.signal.geysera.com/signal-api/v1/audiences/{key}/runs/estimate" \
  -H "Authorization: Bearer sk_sig_..." \
  -H "Content-Type: application/json" \
  -d '{"mode":"prospects"}'
GET/audiences/summaries

One line per audience: the funnel counts a collapsed row shows

Every audience in the workspace with the four counts that head its row — identified, profiled, likely, accepted — and whether its outputs are on.

This exists because a page listing audiences should cost one request, not one per audience. It is deliberately NOT `GET /{key}/overview` repeated: that call also computes Account Watch, the calibration, recent runs and the month's credits, which a collapsed row does not show. Measured on the pilot, per audience: the overview is 3.2s, these counts are 1.4s.

The counts come from the same functions the overview uses, so the two cannot disagree about what "likely" means.

Query parameters

No parameters.

Response fields

audiencesarray of PublicAudienceSummaryrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl "https://app.signal.geysera.com/signal-api/v1/audiences/summaries" \
  -H "Authorization: Bearer sk_sig_..."
Response
{
  "audiences": [
    {
      "key": "string",
      "name": "string",
      "identified": 0,
      "profiled": 0,
      "likely": 0,
      "accepted": 0,
      "outputs": {
        "enabled": false,
        "reason": null
      }
    }
  ]
}
POST/copilot/ask

Ask a question about this workspace's commerce data

Natural-language analysis over the Signal metric layer.

## When to Use - Any question about revenue, AOV, products, pricing, page conversion or what sells together.

## Notes - Returns `refusal` instead of `answer` when the question cannot be answered honestly, because a correlation presented as a cause is worse than no answer. - A causal question ("did X drive Y") is ANSWERED, with an interval and an explicit verdict when that interval includes zero. This note used to say such questions were "refused by design until a causal identification tier exists"; the tier exists and answers, so a caller coding to that sentence would never have asked one. - When a causal question names no date, the analysis SUBSTITUTES the largest recorded price change in the window as the intervention and says so in the answer. That substitution is the contract — not a refusal, and not an analysis of the thing you named. - `trace` lists every tool that ran with its coverage and the exact SQL. - Numbers in `answer` are checked against the tool results before returning; a draft containing an unsupported figure is rejected rather than shipped.

Query parameters

No parameters.

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl -X POST "https://app.signal.geysera.com/signal-api/v1/copilot/ask" \
  -H "Authorization: Bearer sk_sig_..." \
  -H "Content-Type: application/json" \
  -d '{"question":"string"}'
POST/copilot/stream

Ask a question, streamed per analysis step

Identical to `/ask` but emits an SSE event as each stage completes, so the UI can show `planning → running 3 tools → checking → writing` instead of a spinner.

Events: `plan`, `execute`, `verify`, `answer_segment`, `narrate`, `done`, `error`. The `done` event carries the same payload `/ask` returns.

`answer_segment` carries the answer a sentence at a time as it is written. Each segment has already passed the numeric guard — the same check the complete answer faces — so a segment is safe to render the moment it arrives. Concatenating every `answer_segment` yields exactly the `answer` field of the final `done` payload; clients that would rather not stream can ignore these and use `done` alone.

Query parameters

No parameters.

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl -X POST "https://app.signal.geysera.com/signal-api/v1/copilot/stream" \
  -H "Authorization: Bearer sk_sig_..." \
  -H "Content-Type: application/json" \
  -d '{"question":"string"}'
GET/copilot/threads

List this workspace's conversations

Most recently updated first. Powers the history sidebar. Also carries the workspace's display name, which the page greets the user with, and which kinds of data it holds — both ride along here rather than costing a round trip each.

`capabilities` reports whether this workspace has orders, pixel events and identified accounts. A client uses it to offer example questions that are answerable here: a workspace with no connected store has no order source at all, so every commerce question refuses, and offering them is how a new tenant's first four questions all came back empty.

Query parameters

No parameters.

Response fields

threadsarray of PublicThreadSummaryrequired
workspacestring or nulloptional
The workspace's company name, or its domain, or null.
capabilitiesPublicWorkspaceCapabilitiesrequired
What this workspace has connected, and so what can be asked about it. All three false can mean nothing is connected OR that the check itself failed; it is not proof of an empty workspace.

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl "https://app.signal.geysera.com/signal-api/v1/copilot/threads" \
  -H "Authorization: Bearer sk_sig_..."
Response
{
  "threads": [
    {
      "id": "string",
      "thread_key": "string",
      "title": "string",
      "created_at": "2026-09-11T14:23:07Z",
      "updated_at": "2026-09-11T14:23:07Z",
      "question_count": 0
    }
  ],
  "workspace": "string",
  "capabilities": {
    "orders": false,
    "pixel": false,
    "accounts": false
  }
}
GET/copilot/threads/{thread_id}

A conversation and its messages

Full history for one conversation, including each answer's disclosures and the tool trace behind it — so an answer someone acted on months ago is still auditable.

`thread_id` accepts **either** identifier: the `thread_key` you chose and sent to `/ask`, or the server-generated `id` that `/threads` returns. They are different values and both name the same conversation.

A thread belonging to another workspace returns 404 rather than 403: the tenant filter *is* the authorization check, and 404 avoids confirming the id exists. An identifier matching nothing returns 404 too, whatever its shape — it used to return 500 for anything that was not a UUID.

Query parameters

thread_idstringrequired

Response fields

idstringrequired
thread_keystringrequired
titlestring or nulloptional
created_attimestamprequired
updated_attimestamprequired
messagesarray of PublicThreadMessagerequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 404 No conversation with that identifier in this workspace — also returned for a thread owned by another workspace, which avoids confirming the id exists · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl "https://app.signal.geysera.com/signal-api/v1/copilot/threads/{thread_id}" \
  -H "Authorization: Bearer sk_sig_..."
Response
{
  "id": "string",
  "thread_key": "string",
  "title": "string",
  "created_at": "2026-09-11T14:23:07Z",
  "updated_at": "2026-09-11T14:23:07Z",
  "messages": [
    {
      "id": "string",
      "role": "string",
      "content": "string",
      "kind": "string",
      "question_kind": "string",
      "disclosures": [],
      "trace": [],
      "rating": "string",
      "created_at": "2026-09-11T14:23:07Z",
      "turn_id": "string"
    }
  ]
}
GET/recommendations

Suggested next actions that have not been done yet.

Recommended actions that are currently open.

"Open" means what it means in the app: status `new`, or `snoozed` with the snooze elapsed. Read-only — this endpoint does not change a recommendation's status.

Query parameters

No parameters.

Response fields

recommendationsarray of PublicRecommendationrequired
totalintegerrequired
window_daysintegerrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl "https://app.signal.geysera.com/signal-api/v1/recommendations" \
  -H "Authorization: Bearer sk_sig_..."
Response
{
  "recommendations": [
    {
      "id": "string",
      "play_id": "string",
      "kind": "string",
      "scope": "string",
      "priority": 0,
      "action": "string",
      "rationale": "",
      "source": "",
      "evidence": null,
      "updated_at": "2026-09-11T14:23:07Z"
    }
  ],
  "total": 0,
  "window_days": 0
}
GET/visitors

List identified people with name, email, title and company. Contact fields are hidden on the free plan.

Visitors belonging to accounts inside the plan's resolution cap.

Two rules are load-bearing and mirror the dashboard exactly:

* The free-plan rule is the dashboard's, from agents/signal/free_allowance.py: the first 150 people identified each month are shown in full; beyond that contact fields are withheld and a company known only through such people is masked. The paywall has to hold on every surface or it holds on none. * `is_locked` says so explicitly rather than leaving an integrator to guess why the values are null.

Query parameters

pageintegeroptional1–1000, default 1
Page number, from 1. Capped at 1000, so `page x page_size` bounds what offset pagination can reach: 50,000 rows at the default page size and 200,000 at the maximum. For a full export use `page_size=200`; to go beyond 200,000, narrow the query with a filter. Asking for page 1001 returns 422 rather than an empty page, so a walk cannot mistake the ceiling for the end of the data.
page_sizeintegeroptional1–200, default 50
Rows per page, 1-200. The default of 50 is sized for a screen; a full export should ask for 200, which also multiplies how far the 1000-page ceiling reaches.
min_intentinteger or nulloptional0–100
0-100 display scale, as shown in the app.
classificationlead | customer | excluded or nulloptional
Filters on THIS PERSON's status (see `status`). Omitted: everyone except excluded. `excluded`: only them.
segmentprovider | dtc | internal | unclassified or nulloptional
Filters on what the STORE says this person is. `provider` is the trade segment — wholesale, partner, and their applicant/lead roles.

Response fields

visitorsarray of PublicVisitorrequired
totalintegerrequired
pageintegerrequired
page_sizeintegerrequired

Status codes: 200 Successful Response · 401 Missing, malformed, unknown or revoked key · 403 Key lacks the required scope · 422 A parameter failed validation · 429 Rate limit exceeded for this key

Request
curl "https://app.signal.geysera.com/signal-api/v1/visitors" \
  -H "Authorization: Bearer sk_sig_..."
Response
{
  "visitors": [
    {
      "id": "string",
      "company_domain": "string",
      "company_name": "string",
      "intent_score": 0,
      "account_classification": "string",
      "status": "string",
      "orders_count": 0,
      "has_ordered": false,
      "store_segment": "unclassified",
      "store_roles": [
        "string"
      ],
      "resolved_email": "string",
      "resolved_name": "string",
      "resolved_title": "string",
      "employer": "string",
      "first_visit_at": "2026-09-11T14:23:07Z",
      "last_visit_at": "2026-09-11T14:23:07Z",
      "visit_count": 0,
      "is_locked": false
    }
  ],
  "total": 0,
  "page": 0,
  "page_size": 0
}

Asking questions in natural language

With a copilot-scoped key you can ask about your workspace instead of assembling the query yourself. The response carries the answer, the tools that ran, and the figures behind it.

POST /signal-api/v1/copilot/askcopilot
Ask a question about this workspace's commerce data
POST /signal-api/v1/copilot/streamcopilot
Ask a question, streamed per analysis step
GET /signal-api/v1/copilot/threadscopilot
List this workspace's conversations
GET /signal-api/v1/copilot/threads/{thread_id}copilot
A conversation and its messages
Read-only, whatever it is asked. A key reaches the copilot's read tools and never the ones that change a workspace — ask it to invite a teammate and it will explain that it cannot, rather than doing it. It refuses rather than guesses, and a causal question is refused by design: a correlation presented as a cause is worse than no answer.

Pagination

paramsconvention
page (1-based), page_size (default 50, maximum 200)
responseconvention
an array named for the resource (`accounts`, `visitors`) plus total, page, page_size. There is no `items` key.
noteconvention
total is capped at what your plan may resolve, never the raw row count
out_of_rangeconvention
page_size above 200 is rejected with 422 rather than silently clamped, so a script cannot believe it read more than it did
Accounts beyond your plan's monthly resolution cap are not returned, and `total` is capped to match, so a full page may not be the whole set. The response does not report how many exist beyond the cap.

Unknown query parameters are ignored, not rejected. A misspelled parameter returns a normal response with defaults applied. A person notices; a script reports the wrong number confidently.

Rate limits

300 requests per 60 seconds, per key. Exceeding it returns 429.

Errors

Every error carries the same shape:

error_codefield
stable machine-readable code; one of CLIENT_ERROR, CONFLICT, FORBIDDEN, GONE, INTERNAL, METHOD_NOT_ALLOWED, NOT_FOUND, PAYLOAD_TOO_LARGE, PAYMENT_REQUIRED, RATE_LIMITED, UNAUTHORIZED, VALIDATION_ERROR
messagefield
human-readable description
correlation_idfield
echo of X-Correlation-ID; quote it in support requests
detailsfield
optional structured context

Branch on details rather than parsing message. Quote correlation_id when asking us about a specific request.

What is not available yet

  • Writes. No endpoint creates or changes anything. The idempotency and concurrency contracts those need are not built, and shipping writes without them would be a promise we could not keep.
  • Per-key narrowing below the workspace. Every key sees the whole workspace.
  • Webhooks for these resources. Poll, or use Auto-sync Rules in the app.

Discovery

Both of these are generated from the running service. If this page and those ever disagree, they are right.

Create a workspace

Natural language

What it can read

The 55 read tools the copilot can call, published by the service and derived from its own registry. Write tools are absent because an API key cannot reach them — changes are made in the dashboard, or by asking in a signed-in session where each one is described and confirmed first.

aov_decompositionread
Splits AOV into average unit price x average units per order, before vs after a boundary date.
attribution_overviewread
Which channels, sources and campaigns the attributed sessions came from, and what they were worth — the same cached rollup the Attribution page renders.
audience_acceptedread
The people ACCEPTED into an audience, newest decision first — by the model from their professional profile (`automatic`, with its `reason`) or by the team: name, email, business email (their own address when it is at a work domain), title, employer, industry, last order.
audience_account_watchread
The audience's confirmed members (people the store's own role marks as members) worth a call: `late` — their reorder is overdue against their own ordering rhythm — and `browsing_without_buying` — identified on the site in the window with no order since.
audience_digestread
The audience's daily email — the people accepted since the last one, and those left to review: whether the person asking is subscribed, how many teammates are, and what was sent each of the last seven days — or why nothing was (not calibrated, the workspace's email is switched off, nobody new).
audience_overviewread
The whole pipeline for one audience, as the Audience Classification page shows it: identified (people who read its pages, 90 days), profiled (of those, with a professional profile / company record), classified (likely, how many of them read the pages, candidates, known accounts and how many became late), reviewed (accepted, rejected; `to_review` — likely the model's reading did not settle; `nothing_known` — readers nothing known about matches; and how the model's decisions stand: `automatic`, `overruled` by the team, `confirmed`), the latest calibration and whether the definition changed since, whether exports are on and why not, runs in progress, and this month's credits.
audience_prospectsread
People browsing this site who look like a professional audience the store does not yet sell to — e.g.
audience_rejectedread
The people REJECTED from an audience, newest decision first — by the model from their professional profile (`automatic`, with its `reason`) or by the team — with their title, employer and email.
audience_run_estimateread
Before starting an enrichment run: how many people it would look up and the most enrichment CREDITS (not money) it could spend.
audience_runsread
Recent enrichment runs for an audience: mode, status, credits spent, what was found, and — for calibration — how many known members and how many non-members the audience labels likely.
causal_impactread
Did a dated change actually CAUSE a metric to move? Fits a Bayesian interrupted time series: models the pre-change trend, predicts what would have happened without the change, and reports the difference with a credible interval.
credit_usageread
Credits used and left this month (UTC), every limit with when it resets and which runs out first, what started the lookups (audience runs, enrichment rules, a person, bulk lookups) with how many were found and how many were business contacts, and the latest lookups line by line.
customer_accountsread
The store's own customers by email, with the role the store gives them (provider, wholesale, …) and their LIFETIME order count, revenue and AOV.
customer_countsread
How many distinct people bought, how often they came back, and how many were new.
data_coverageread
What this workspace can and cannot answer for a window: order count, pixel volume, how many orders link to a pixel session, and which days the order sync missed.
deletion_statusread
Check whether a deletion or opt-out request has been honoured for one email address, and when.
discount_codesread
Which discount codes were used, with orders, customers, revenue, discount given and AOV per code.
dry_run_sync_ruleread
Show exactly which contacts a rule WOULD push, without pushing anything.
enrichment_configread
Who this workspace enriches and what it can still spend.
esp_connectionread
Every app this workspace can connect — email platforms, the store, Slack, webhooks — with each one's status (connected, attention: set up but not working, not_connected, coming_soon).
esp_syncread
Whether ESP sync is on (people are sent to the email platform only while it is), when it was turned on, who may be sent today counted by reason (subscribed / company email address / found work email) and how many wait for a known country, and where a workspace OWNER turns it on or off (fix_label).
get_accountread
Everything known about ONE account by domain: firmographics, its visitors, orders and ESP contacts.
get_conversationread
Read back one past conversation — its questions, answers and the caveats each answer carried.
identification_statusread
Is identification actually running right now, and when did it last resolve anyone? Use when a workspace says visitors stopped appearing — a quiet pipeline and quiet traffic look identical on the accounts page, and this is what tells them apart.
intent_pagesread
The page rules this workspace uses to score buying intent, and what share of its real traffic they match.
journey_pathsread
How visitors move BETWEEN pages — the page-to-page graph behind the Journey view, reading the same nightly rollup the page reads.
lapsing_accountsread
Which of the store's OWN known buyers in a segment have stopped ordering.
learned_plansread
What this assistant has learned to do for recurring questions in this workspace — which tools it reuses, how many times each was confirmed, and which have been retired.
list_alert_destinationsread
Slack destinations receiving this workspace's alerts, and whether each is active.
list_api_keysread
The workspace's API keys — id, name, prefix, last used.
list_audiencesread
Every audience this workspace has defined — name, key, title and employer terms, pages, known-member segment — and `store_roles`: what set_audience's confirmed_roles may name (the store's roles, Geysera's, and each product sold, as 'product:<id>' labelled 'Bought …').
list_conversationsread
Your own past conversations in this workspace, most recently updated first, with each one's title, question count and when it was last touched.
list_ordersread
The individual orders themselves, most recent first, with status, total, items, discount and the customer.
list_teamread
Who has access to this workspace: members with their roles, invitations still pending, and how many seats are left.
list_webhooksread
Registered webhook endpoints and whether each is enabled.
membership_conversion_pathsread
What is bought together.
open_recommendationsread
The workspace's open recommended actions, from both the journey and attribution surfaces.
order_sourcesread
Where orders came from according to the STORE, and how they were checked out: the channel it attributed each order to (utm, organic, referral, typein), whether the order was a subscription renewal, the payment method and the shipping method — with orders and revenue for each, plus what shipping was charged and how much revenue shipped free.
page_funnelread
Per page: sessions that saw it, and how many added to cart, reached checkout and purchased in that same session.
page_to_product_attributionread
Which pages were seen in the same session as a purchase, optionally restricted to orders containing a given product.
preview_enrichment_ruleread
Count an UNSAVED enrichment rule step by step — how many people each step leaves — before creating it.
price_historyread
When a product's price actually changed.
product_catalogueread
The products the store SELLS — every item in the catalogue with its SKU, price, categories and stock status, whether or not it has ever sold.
product_performanceread
Revenue, orders, units and quantity-weighted unit price per product.
recall_factsread
What this workspace has taught the assistant, and when.
revenue_and_aovread
Revenue, order count, AOV, discount and units per period (day/week/month).
search_accountsread
The accounts board: search by name or domain, filter by classification and minimum intent, sort, paginate.
search_visitorsread
Individual identified people, with the filters the Visitors page offers: text search, classification, minimum intent, whether they have ordered, whether they have been contacted, which intent page they hit, store segment and when they were last seen.
subscriptionread
This workspace's GEYSERA plan — what they pay us — its status, when it next renews, and whether it is set to cancel at the end of the period.
sync_rule_targetsread
The lists an auto-sync rule can send to in a connected email platform, read live from it (marking the ones Geysera made), or — with weekly_csv — the owners and admins who may receive a weekly CSV.
sync_rulesread
The auto-sync rules that push identified accounts to the connected ESP: their filters, status and last run.
visiting_accountsread
Which organisations visited the site, ranked by intent score, with their visits, visitors and any orders.
visitor_enrichmentread
What enrichment found for ONE visitor, by their email address, and whether it has expired.
visitor_journeyread
The pages ONE visitor viewed, in order, with time on each.
workspace_setupread
Is this workspace actually working? Store connection, onboarding progress, pixel status, identification health and the current plan and usage, in one call.

55 tools are listed above. 55 are not.

They change the workspace. An API key reaches read tools only, whatever it asks for — the planner's tool list is filtered by how you authenticated, so a key cannot name one. Listing them would advertise a capability that always refuses.

A signed-in dashboard session. Changes are proposed first: the turn returns pending_confirmation describing the effect, and nothing runs until it is approved.

More documentation

Guides & case studies

This page covers the endpoints. Longer-form documentation lives on the app host, next to the service it describes, at https://app.signal.geysera.com/developers.

Get started

  • Quickstart

    Create a key and make your first call.

  • Install the pixel

    The tag, what it collects, and how consent works. Prerequisite for every workflow here.

Guides

  • Developer guide

    The three surfaces, conventions, the copilot, webhooks, and the things that will surprise you.

Workflows

Playbooks

Case studies