POST an event — a pricing page view, a form submission, a product usage milestone — and Topo stores it, matches it to a contact or account in your workspace, and makes it actionable: playbooks with an event trigger run within minutes, and every event_name you turn into a custom event signal feeds its own list of people or companies.
Unlike the third-party signals Topo detects on its own (job changes, funding rounds, technology adoption), first-party events come only from you. They are usually the strongest intent you have, because they describe behaviour in your own product rather than a public event.
Ingestion is deliberately forgiving: every well-formed event is accepted with 202 Accepted and stored, then matched to a contact or account asynchronously. Reading events back with GET /v1/events is how you confirm what Topo received and how far resolution got.
Topo groups first-party events strictly by event_name. That string is the key that wires ingestion to everything downstream — custom event signals in Settings → Signals, their dedicated lists, and playbook event triggers all match on the exact, case-sensitive event_name you send. Two different names are two independent event types, even when the payloads look similar.
Before you start
1
Create an API key
In the Topo dashboard, go to Settings → Developers → API keys, click Create Key, and grant
events:write to ingest and events:read to read events back. The raw key is shown once — see Authentication.2
Check the scopes on your key
GET https://api.topo.io/v1/me returns the key’s scopes. It needs no scope of its own, so it always works.3
Point your integration at the base URL
Every endpoint on this page is relative to
https://api.topo.io/v1.4
Decide your event names
One stable, machine-readable name per event type (
pricing_page_visit, trial_started). You bind a name to a signal later, and that binding cannot be changed — see Custom event signals.Ingest an event
Every event needs a name, the time it occurred, and asubject that tells Topo who it is about. All field names are snake_case.
202 Accepted:
accepted is always true on a 202; it confirms the event was durably stored, not that it has been correlated or scored yet. idempotency_key is the key Topo deduplicated the event under — first-party:<organization_id>:<external_event_id or content hash>.
Request fields
string
required
Your name for the event, such as
pricing_page_visit or trial_started. Between 1 and 255 characters. Use a stable, machine-readable name — Topo groups events, feeds signal lists, and matches playbook triggers on this exact value. Matching is case-sensitive and exact: Pricing_Page_Visit and pricing_page_visit are two different event types.string (RFC 3339)
required
When the event happened in your system, not when you sent it. Use UTC with a
Z suffix or an explicit offset. It is the timestamp Topo displays, sorts on, and filters with occurred_at_after / occurred_at_before. Backdating is safe: custom event signals collect events in the order Topo received them, so a historical import is picked up on the next poll like any other event.object
required
Identifies who or what the event is about. Supply at least one identifier; a request with an empty subject is rejected with
400. Which identifier you send also decides which signal level can use the event — see Subject resolution and Which subject field each level needs.string
Your identifier for this event, up to 255 characters. Supply it to make retries safe — see Idempotency.
object
Free-form JSON describing the event. Topo stores it whole and hands it to playbooks as the trigger payload, so include whatever context matters.
The request rejects unknown top-level fields with
400. Send exactly the fields above.GET /v1/events always returns your payload exactly as you sent it. When a custom event signal picks up an event, Topo copies the payload into the signal event’s metadata exactly as you sent it too — nested objects, arrays, and null values included, with no limit on the number of keys and no truncation of long strings. The only constraint is total size: if the payload serializes to more than 32 KB of JSON, the copy is left out of the signal metadata. The event is never dropped — it still creates a signal event with its name and subject — and the complete payload always remains available from GET /v1/events.
Read events back
GET /v1/events and GET /v1/events/{event_id} let you verify what Topo stored and how resolution went — useful when you are wiring up an integration or debugging a subject that never matched.
subject you submitted plus what Topo made of it: resolution_status, and once resolution succeeds, contact_id and account_id — org-scoped Topo identifiers, never raw CRM or person ids. received_at is when Topo stored the event; resolved_at is when subject matching finished.
List responses use the standard pagination envelope and default to occurred_at descending, so the newest events come first. occurred_at is the only supported sort_by value. Narrow the list with these filters:
Resolution status
Resolution runs on a background queue that drains every couple of minutes, so an event normally leaves
PENDING within about two minutes of the 202.
Subject resolution
Topo accepts every well-formed event, even when the subject does not match anything you know yet. Resolution runs asynchronously and is best effort. When several subject fields are supplied, Topo tries the contact identifiers in this order and stops at the first match:emaillinkedin_urlexternal_contact_id, via your connected CRM
company_domain first, then external_company_id via your CRM, and finally the employer of the contact that just resolved.
Matching runs against Topo’s whole people and company directory, not just the contacts already in your workspace. When the subject matches a person or company that is not in your workspace yet, Topo creates the contact (and its account) for you as part of resolution — the same way a lead search import would. An event can still never resolve onto another organization’s records: the contact Topo creates or links is always your workspace’s own.
You do not need to import a contact before sending events about them — a subject that matches a person Topo knows gets its contact created automatically. An event only stays
UNRESOLVED when the identifiers match no one in Topo’s directory, and it still counts for custom event signals: it lands on the signal’s list as a raw email or domain entry, just without a link to a Topo contact or account.Idempotency
Ingestion is idempotent, so retrying after a timeout or a redelivery from your queue never produces a duplicate signal. Topo deduplicates on two things:external_event_id— the reliable option. Pass your own event id and Topo treats any replay of that id as the same event.- Event content — if you omit
external_event_id, Topo hashes the event itself, so byte-identical resends are still collapsed. Change any field and you get a new event.
202 Accepted and the same idempotency_key as the original call, and the event is neither stored nor processed a second time.
Custom event signals
Ingesting events stores them and runs your playbooks. To also get people or companies collected in a list, turn anevent_name into a custom event signal. Each signal watches exactly one event_name and feeds a dedicated list that Topo provisions for it, named Signals · . Without a custom event signal, an ingested event never becomes a buying signal and never lands on a list.
Create the signal
1
Open Settings → Signals
Scroll to the Custom events section and click Add custom event signal.
2
Enter the event name
Type the
event_name exactly as you send it — matching is case-sensitive. The field suggests names Topo has already received from you, so sending one event first makes this step self-checking. You can also create the signal before sending anything: the first poll backfills the history for that name.3
Choose who the signal tracks
People creates a contact list; Companies creates an account list. Pick the one that matches the subject field you send (see the table below).
4
Save
Topo creates Signals · and links it from the signal’s row, next to a toggle that enables or disables the signal.
Which subject field each level needs
A skip is silent and permanent: each poll advances a watermark past the events it read, so a
company_domain-only event will never appear on a People list, even after you fix your integration. Send email and company_domain whenever you know both — resolution has more to match on, and a People entry then also carries the company.
When entries appear
Custom event signals are polled, not pushed, so list entries are never instant:- Topo checks for due signals every hour, so the first poll runs within an hour of creating the signal.
- After that, custom event signals poll once a day. An event ingested just after a poll waits for the next one.
- The first poll backfills every event already stored under that name, oldest first.
- A poll reads at most 500 events per signal. A larger backlog is worked through over the following polls, 500 per day.
What lands on the list
- People — one entry per email (or per matched person when the event has no email), carrying the company domain from the event.
- Companies — one entry per domain.
- When resolution succeeded, the entry is linked to the matched Topo contact or account. When it did not, the entry keeps the raw email or domain you sent and stays unlinked — Topo does not link it later.
- First-party events are trusted rather than scored against your ICP, so every event a signal keeps lands on the list.
POST with the same external_event_id is not stored twice (see Idempotency), and an event re-read by a later poll never creates a second entry.
Example: two event types, two lists
Anonymous traffic is company-level; a form submission is people-level. Send them under two names and wire each to its own signal.website_visit at the Companies level and filled_form_on_landing_page at the People level. Within the hour, the account list Signals · website_visit holds acmecorp.com and the contact list Signals · filled_form_on_landing_page holds jane.smith@acmecorp.com. Each signal ignores the other’s events.
After acceptance
202 Accepted means Topo has taken responsibility for the event, not that the work is finished. Everything downstream happens after the response is sent:
Playbooks run whether or not the subject resolved — the trigger payload carries the raw
email, linkedin_url, and company_domain you sent.
Track resolution with webhooks
Subscribe toevent.resolved on a webhook whose sequence_template_ids is null (all templates). Topo delivers a resource-family payload when subject matching finishes — event_id, event_name, resolution_status, and once resolved, contact_id and account_id on the payload. This is the push counterpart to polling GET /v1/events for the same resolution fields.
Narrowed subscriptions (specific sequence_template_ids) do not receive resource-family events.
Troubleshooting
Errors
Validation failures return
400 with the standard ValidationIssue envelope, not 422. See Errors & Limits for the full error shape, the per-organization rate limits, and the X-RateLimit-* headers.
Example validation error (400)
403)