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

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.

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 identified accounts

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 | competitor | 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"
    }
  ],
  "total": 0,
  "page": 0,
  "page_size": 0
}
GET/attribution

Attribution rollup for a window

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 remaining fields are absent rather than zero — do not read a missing rollup as no attribution.
computed_attimestamp or nulloptional
When the rollup was last computed.
available_windowsarray of integer or nulloptional
Only present when has_data is false: the windows that DO have a rollup, so a caller can retry with one that exists.
totalsunknown or nulloptional
funnelarray of unknown or nulloptional
Visitors -> Identified -> High intent. Nested by construction.
channelsarray of unknown or nulloptional
sourcesarray of unknown or nulloptional
Where visitors came FROM. Answers where to spend.
campaignsarray of unknown or nulloptional
landing_pagesarray of unknown 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 unknown or nulloptional
trendarray of unknown or nulloptional
Dense over the window, so real gaps stay visible.
landing_channelarray of unknown or nulloptional
revenueunknown 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
}
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 — a causal question ("did X drive Y") is refused by design until a causal identification tier exists, because a correlation presented as a cause is worse than no answer. - `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 · 422 Validation Error

Request
curl "https://app.signal.geysera.com/signal-api/v1/copilot/ask" \
  -H "Authorization: Bearer sk_sig_..."
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 · 422 Validation Error

Request
curl "https://app.signal.geysera.com/signal-api/v1/copilot/stream" \
  -H "Authorization: Bearer sk_sig_..."
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.

Status codes: 200 Successful Response

Request
curl "https://app.signal.geysera.com/signal-api/v1/copilot/threads" \
  -H "Authorization: Bearer sk_sig_..."
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

Status codes: 200 Successful Response · 422 Validation Error

Request
curl "https://app.signal.geysera.com/signal-api/v1/copilot/threads/{thread_id}" \
  -H "Authorization: Bearer sk_sig_..."
GET/recommendations

Open recommended actions

Catalog-grounded recommendations that are currently actionable.

Queries signal_journey_recommendation directly rather than importing the dashboard's recommendations service. That service lives on an unmerged branch (PR #98), and importing it here would have made this endpoint raise ImportError at runtime while every unit test passed — a cross-branch dependency is not a dependency, it is a deferred outage.

"Open" means the same thing it means in the app: status 'new', or 'snoozed' with the snooze elapsed. Status mutation is deliberately NOT exposed — acting on a recommendation carries an optimistic-locking contract that needs its own design rather than being bolted onto a read-only v1.

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 visitors

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

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

* The accessible set is the earliest cap accounts by first_seen_at. Skipping this would let a free tenant read their entire book through the API while the UI blurs it — the paywall has to hold on every surface or it holds on none. * Free-tier contact fields are withheld. `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 | competitor | excluded or nulloptional
Filters the ACCOUNT's classification, not the person's.
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": [
    {
      "company_domain": "string",
      "company_name": "string",
      "intent_score": 0,
      "account_classification": "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. The response reports how many exist so the omission is never silent.

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 36 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.
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.
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.
dry_run_sync_ruleread
Show exactly which contacts a rule WOULD push, without pushing anything.
enrichment_configread
Who this workspace enriches, under what conditions, and what it is spending.
esp_connectionread
Which email platform this workspace is connected to, if any, and whether the connection is verified.
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.
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_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_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
Which OTHER products are in the basket when a given product is bought.
open_recommendationsread
The workspace's open recommended actions, from both the journey and attribution surfaces.
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.
price_historyread
When a product's price actually changed.
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 plan status, when it next renews, and whether it is set to cancel at the end of the period.
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.

36 tools are listed above. 43 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