Skip to content
v1
Get an API key

Custom events

View as Markdown

Your own systems can tell Funnel One when something happens: a trial starts, a plan changes, an invoice is paid. Each event is recorded against a company or contact that is already in your workspace, and you can then build custom signals on it.

Endpoint Scope What it does
POST /api/v1/events events.send Send one event.
POST /api/v1/events/batch events.send Send up to 100 events in one request.
GET /api/v1/events events.send List the events received in the last 30 days.
GET /api/v1/event-types events.send List the event names your workspace has received.

events.send (“Send custom events from the API”) is held by the Admin role by default. A key only has it while the person who owns the key has it too. See Scopes.

  • An event never creates a company or a person. It is matched to a company or contact that already exists in your workspace. When nothing matches, the event is kept for 30 days as unmatched so you can see why, and it does nothing else. To add a new person, send a lead.
  • An event may not carry personal details. Put the person in contact, which is only used to look them up. An event whose properties hold personal data is refused (see Personal data).
  • Every event has its own id. Sending an id that was already recorded in the last 30 days answers duplicate and writes nothing, so retrying is always safe.
Terminal window
curl -X POST https://app.funnelone.ai/api/v1/events \
-H "Authorization: Bearer $F1_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "evt_trial_4812",
"name": "trial_started",
"occurred_at": "2026-09-29T12:00:00Z",
"company": { "domain": "acme.com" },
"contact": { "email": "jane@acme.com" },
"properties": { "plan": "pro", "seats": 12, "annual": true }
}'
Field Required Rules
id Yes Your own id for this event: 1 to 100 visible ASCII characters, unique in your workspace.
name Yes What happened. Starts with a lower-case letter, then a-z, 0-9 and _, at most 48 characters. For example trial_started.
occurred_at No When it happened, an ISO 8601 date and time. At most 30 days ago and at most 5 minutes ahead. Default: now.
company One of company, contact or visitor Exactly one of { "id": 123 } or { "domain": "acme.com" }.
contact One of company, contact or visitor Exactly one of { "id": 456 } or { "email": "jane@acme.com" }. A contact given without a company supplies the company.
visitor One of company, contact or visitor { "id": "<visitor id>" }: the id Funnel One’s website tracker gave this browser, 1 to 64 letters, digits and dashes. See Events about a website visitor.
properties No Up to 50 properties. Each name starts with a lower-case letter, then a-z, 0-9 and _, at most 40 characters. Each value is text (up to 512 characters), a number, or true/false. At most 4 KB in all.

Any other field, including organization_id, is refused. The workspace is always the one your key belongs to.

A domain is matched the same way Funnel One matches domains everywhere else, including the other domains a company is known by. A company or contact id from another workspace matches nothing, exactly like an id that does not exist.

An Idempotency-Key header is optional here: the event’s id already makes a retry safe.

Answers:

  • 202 { "id", "status", "event_id" }, where status is accepted (it matched), unmatched (recorded, but nothing in your workspace matched it), pending_identity (it named only a website visitor who has not been identified yet: recorded, and waiting for them) or duplicate (this id was already recorded; nothing was written). The answer never says which company or person an event matched.
  • 422 when the event is refused, with the reason in error.code (see Refusals).

Send 1 to 100 events at once, in a body of at most 256 KB:

{ "events": [ { "id": "evt_1", "name": "plan_changed", "company": { "id": 1234 } } ] }

Each event is judged on its own. One refused event does not stop the others. The answer is always 202, with counts and one result per event, in the order you sent them:

{
"accepted": 1, "duplicate": 0, "unmatched": 0, "pending_identity": 0, "refused": 1,
"results": [
{ "id": "evt_1", "status": "accepted", "event_id": 90211 },
{ "id": "evt_2", "status": "refused", "error": { "code": "pii_value", "message": "…", "param": "properties.note" } }
]
}

Retrying a whole batch is safe: events already recorded come back as duplicate.

If your site runs Funnel One’s website tracker, an event can name the visitor instead of a company or a person. This suits things your servers see before you know who someone is, such as a trial started before sign-up.

{ "id": "evt_trial_4813", "name": "trial_started", "visitor": { "id": "3f1c2b1a-0d4e-4f5a-9b8c-7d6e5f4a3b2c" } }
  • If the visitor has already told you who they are, by filling in a form, booking a meeting, clicking a link in one of your emails, or through a signed F1A.identify, the event is accepted for that person and their company, straight away.
  • Otherwise the event waits (pending_identity). It is recorded, and it does nothing yet. When the visitor identifies themselves in one of those ways, the event is matched to them and becomes a fact. Because it arrives late, it counts toward signals and scores but does not start alerts, workflows or motions.
  • A visitor we only guessed at does not count. A company identified from the visitor’s browsing, or an unsigned F1A.identify, is not enough: the event keeps waiting for the person.
  • Waiting lasts at most 30 days. An event not matched by then is never matched, and it is deleted with the rest of your events after 30 days.
  • A visitor id the tracker has not seen yet still waits, because your server can easily send before the browser does. It is matched once the tracker sees that id and the visitor identifies themselves.
  • An event names what it names. If it also names a company or contact that is not in your workspace, it is unmatched; the visitor is not used in its place. With a company and a visitor, the visitor supplies the person only if they work at that company.
  • Nothing is created. A visitor id only borrows who the visitor already is in your workspace. A visitor id from another workspace matches nothing.

In the browser, on a page that runs the tracker:

const visitorId = window.F1A && window.F1A.getVisitorId();

It returns the id, or null when the tracker is not tracking this visitor: the browser sends Do Not Track or Global Privacy Control, your site waits for cookie consent (data-consent="required") and it has not been given, the visitor refused with F1A.consent(false), or the tracker has not started yet. When it is null, send the event without visitor, or not at all. Pass the id to your server with your own request.

On your server, you can instead read the _f1vid cookie from a request the browser makes to your own site (the cookie belongs to your domain, so it arrives with your own first-party requests). Only use it where the tracker itself is allowed to track the visitor.

In GET /api/v1/events, a matched waiting event carries resolved_at, the time it was matched. The visitor id itself is never returned.

A signed F1A.identify for someone who is not in your workspace yet adds them: the person, and their company when the email has a business domain. They are added the way a lead sent to POST /api/v1/leads is, under your lead-handling settings (enrichment runs only if you switched it on), and the visitor’s waiting events are then matched to them.

It adds someone only when the token your server signs carries a jti, a unique id for that token (a random UUID is fine):

{ "email": "dana@acme.com", "site": "sk_site_…", "iat": 1790000000, "exp": 1790003600, "jti": "4f1c2b1a-0d4e-4f5a-9b8c-7d6e5f4a3b2c" }
  • Use each token once. A token is tied to the first browser that sends it. Sent again from that browser it changes nothing; sent from a different browser it counts only as an unverified claim and adds nobody.
  • A token without jti still identifies someone already in your workspace, exactly as before. It never adds anyone.
  • An unsigned F1A.identify, a token that does not verify, and a link click from one of your emails never add anyone.
  • At most 500 people a day are added this way per workspace. Past that, identify still recognises people you already have.
  • first_name, last_name and company passed to F1A.identify are used for the new person’s name and company name.

The events your workspace received in the last 30 days, newest first. Use it to check what arrived, and why an event was unmatched.

Query Means
limit Events per page, 1 to 100. Default 50.
cursor The nextCursor of the previous page.
name Only events with this name.
status accepted or unmatched. A row itself can also be pending_identity.
{
"rows": [
{
"id": "evt_trial_4812", "event_id": 90210, "name": "trial_started",
"status": "accepted",
"occurred_at": "2026-09-29T12:00:00.000Z", "received_at": "2026-09-29T12:00:01.000Z",
"company_id": 1234, "contact_id": 55, "domain_hint": "acme.com",
"resolved_at": null,
"properties": { "plan": "pro", "seats": 12, "annual": true }
}
],
"nextCursor": "…",
"total": 132
}

id is your own id; event_id is the id Funnel One recorded it under. domain_hint is the domain the event named. An email address is never stored, so it is never returned. Keep passing nextCursor as cursor until it is null. A cursor this list did not issue answers 400 invalid_cursor.

Every event name your workspace has received (at most 200), in alphabetical order, with limit (1 to 100, default 100) and cursor as above.

{
"rows": [
{
"id": 12, "name": "trial_started", "label": "Trial started",
"status": "active", "signal_fact": "custom_trial_started",
"properties": { "plan": "string", "seats": "number", "annual": "bool" },
"first_seen_at": "2026-09-01T09:30:00.000Z", "last_seen_at": "2026-09-29T12:00:01.000Z"
}
],
"nextCursor": null,
"total": 1
}
  • label is the name people see in Funnel One. An admin can rename it.
  • status is active, archived (still recorded, but hidden from the Signals builder) or blocked (events with this name are refused).
  • signal_fact is what a signal calls the event. Its properties are prop.<key>. See Custom signals.
  • properties gives the type each property first arrived with: string, number or bool.

The first event with a new name registers that name, with the type of each of its properties. After that:

  • a property keeps its first type. Sending seats as text after it arrived as a number is refused (property_type_mismatch);
  • one name can have at most 50 different properties (too_many_properties);
  • a workspace can have at most 200 names. A new name past that is refused (event_type_limit); archive or reuse a name first.

Personal details belong in contact, never in properties. An event is refused when:

  • a property’s name is one of email, e_mail, phone, mobile, name, first_name, last_name, full_name, address, street, zip, postcode, ssn, password, secret, token, api_key, card, iban, dob, birth_date, ip or ip_address (pii_property); or
  • a property’s value looks like an email address or a card number (pii_value).
  • 1,000 events a minute and 100,000 a day (the day is UTC) per workspace, counted by event, so a batch of 100 counts as 100. Past either: 429 events_rate_limited with Retry-After. Nothing in that request was recorded.
  • Your workspace’s earlier events are still being processed: when 20,000 of them are waiting, new ones answer 429 ingest_backlogged with Retry-After: 60.
  • Every request also counts toward the usual per-key and per-workspace request limits. See Limits.
  • Events are kept for 30 days.
Status Code Means
400 invalid_body A field is missing, unknown, or the wrong type. param names it.
413 batch_too_large More than 100 events, or more than 256 KB.
422 invalid_request A rule the schema cannot check, such as a domain that is not a domain, or properties over 4 KB.
422 event_too_old occurred_at is more than 30 days ago.
422 event_in_future occurred_at is more than 5 minutes ahead.
422 event_blocked Events with this name are blocked in your workspace.
422 pii_property A property’s name is on the personal-data list.
422 pii_value A property’s value looks like an email address or a card number.
422 property_type_mismatch A property arrived with a different type than before.
422 too_many_properties The name would have more than 50 different properties.
422 event_type_limit The workspace already has 200 names.
429 events_rate_limited 1,000 events this minute or 100,000 today. Wait Retry-After.
429 ingest_backlogged Earlier events are still being processed. Wait Retry-After.
503 events_unavailable Events cannot be accepted right now. Retry after Retry-After; every event keeps its id, so nothing is recorded twice.

In a batch, the 422 codes appear on each refused event’s error instead.