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.
0.1.0Every 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 "https://app.signal.geysera.com/signal-api/v1/accounts" \
-H "Authorization: Bearer sk_sig_..."https://app.signal.geysera.com/signal-api/v1, as the service reports it in its own spec. We do not hardcode it here.Authentication
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.
/accountsList 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
curl "https://app.signal.geysera.com/signal-api/v1/accounts" \
-H "Authorization: Bearer sk_sig_..."{
"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
}/attributionAttribution 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
curl "https://app.signal.geysera.com/signal-api/v1/attribution" \
-H "Authorization: Bearer sk_sig_..."{
"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
}/copilot/askAsk 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
curl "https://app.signal.geysera.com/signal-api/v1/copilot/ask" \
-H "Authorization: Bearer sk_sig_..."/copilot/streamAsk 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
curl "https://app.signal.geysera.com/signal-api/v1/copilot/stream" \
-H "Authorization: Bearer sk_sig_..."/copilot/threadsList 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
curl "https://app.signal.geysera.com/signal-api/v1/copilot/threads" \
-H "Authorization: Bearer sk_sig_..."/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
curl "https://app.signal.geysera.com/signal-api/v1/copilot/threads/{thread_id}" \
-H "Authorization: Bearer sk_sig_..."/recommendationsOpen 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
curl "https://app.signal.geysera.com/signal-api/v1/recommendations" \
-H "Authorization: Bearer sk_sig_..."{
"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
}/visitorsList 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
curl "https://app.signal.geysera.com/signal-api/v1/visitors" \
-H "Authorization: Bearer sk_sig_..."{
"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
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
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.
- https://app.signal.geysera.com/agent-api/capabilities — every action a key can call, with the scope each needs. Public, no key required.
- https://app.signal.geysera.com/signal-api/v1/openapi.json — OpenAPI for the REST endpoints.
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
- Receive identified visitors by webhook
Get a POST the moment a visitor is identified, and not miss any.
- Export and keep in sync
Pull the account list once, then keep it current without refetching it all.
- Spend enrichment credits only where they pay back
Stop paying to enrich visitors you will never contact, without losing the ones you would have called.
- Prove a change actually worked
Did the price rise move AOV, or was it the season? The one analysis that supports a causal claim.
- Give Signal to any LLM as tools
Four tool definitions and a loop. Works with Claude, GPT, Gemini, OpenRouter or a local model.
- Ask questions in natural language
Use the copilot from code, and read the parts of the answer that matter.
- Alert your team when a target account appears
Filter the webhook against your named-account list and post to Slack within minutes.
- Build a daily call list
One call, a ranking you can read, and a prompt that is willing to say leave this one alone.
- Write a first-touch email from what they read
An opener grounded in real behaviour, with the SKIP rule that stops it inventing one.
- Spot a buying committee
Three people from one company in a week is one deal, not three leads.
- Notice an account going quiet
The cheapest churn signal you have, weeks before the renewal conversation.
- Fill your CRM with who is actually visiting
Compare the two account sets first; the gap is usually not the one you assumed.
- Find the pages that create pipeline
Rank content by who it brings rather than how many, and read the coverage first.
- Score and route inbound leads
Use what someone did before the form to decide who should get it.
- Keep an ad audience in sync
Suppress customers, retarget high intent, and the consent questions to answer first.
- A weekly revenue brief your LLM writes
Gather deterministically, write with a model, and propagate the disclosures.
- Watch your own data quality
The workflow that protects every other workflow: knowing when the data went quiet.
Playbooks
- Account-based marketing, end to end
Six stages, the two that carry the value, and the human gate to keep.
- Reach the buyer while the signal is warm
Identification is hourly, not instant — and the thing that decays is days, not minutes. Design for same-day, every day.
- Win back the customers who are already returning
Your store knows who lapsed. Signal knows which of them is back on the site this week — measured, that is 33 people, not 8,926.
- Protect your margin when you change prices
AOV rises after a price rise by arithmetic, not evidence. What to check instead, and the one number Signal cannot give you.
- Turning content into pipeline
Measure content on who it brings, then prove the change with a causal test.
- Let an LLM run your workspace
The response contract, refusals, the confirmation gate, and what a key can never do.
Case studies
- Most of your visitors were not people
69% of one account's events were unflagged crawlers.
- The pipeline that reported success while doing nothing
A job that caught its own failure and completed, for days.
- Why attribution covers less than you think
The ceiling is structural, and knowing the number is the useful part.