Skip to main content
The Events API is how your systems tell Topo that something happened. You 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 a subject that tells Topo who it is about. All field names are snake_case.
A successful call returns 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.
How much of the payload reaches a signal. 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.
Every event carries the 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:
The same data is available without writing any code: open Settings → Developers → Ingested events in the Topo dashboard to see the latest events, their subjects, and their resolution status.

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:
  1. email
  2. linkedin_url
  3. external_contact_id, via your connected CRM
The account is resolved separately: 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.
Resolution happens once, and only once. An UNRESOLVED event is never re-resolved when the contact or company appears in your workspace later. If you need the match, send the event again (with a new external_event_id, since the original id is already deduplicated).
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.
A deduplicated replay is not an error: it returns the same 202 Accepted and the same idempotency_key as the original call, and the event is neither stored nor processed a second time.
Send external_event_id whenever your source system has an event id. Content-based deduplication cannot tell a genuine repeat action apart from a retry — two real pricing page visits with an identical timestamp and payload collapse into one signal.

Custom event signals

Ingesting events stores them and runs your playbooks. To also get people or companies collected in a list, turn an event_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.
One signal per event name, and the name and level are fixed at creation. A second signal for a name you already track is rejected, and so is an update that changes the name or the level — both decide which list the signal feeds, and changing them would orphan everything already collected. If you need the same behaviour tracked at both levels, ingest it under two event names (for example demo_requested for people and demo_requested_account for companies).

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.
Ingestion idempotency and list deduplication are separate layers. A retried 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.
In Settings → Signals, create 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 to event.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.
Because resolution is asynchronous, a 202 does not confirm the subject matched a record in your workspace. Do not treat ingestion as a lookup — use the Contacts API to check whether a person exists in Topo, or poll GET /v1/events to see how an event resolved.
To watch what happens next, poll the Activities API or subscribe to Webhooks so downstream tasks and hot leads reach your systems as they are created.

Troubleshooting

Settings → Developers → Ingested events shows the latest events with their subject, resolution status, and timestamps — the fastest way to confirm ingestion without writing a script.

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)
Example scope error (403)