Server-side events
Some conversions never happen in the browser. A Shopify app install finishes in an OAuth callback, a subscription starts in a webhook handler, an invoice is paid on a schedule. The server-events API lets your backend send those moments to Tiny Funnel as named events, and each one is attached to the visitor who caused it.
A server event can never be a funnel's first sight of someone. It attaches to a visitor your funnel has already seen through the script tag, or it is dropped. That is deliberate: an event with no browsing behind it has no entry page, no referrer and no journey, and would only muddy your numbers.
The endpoint
POST https://tiny-funnel.com/api/v1/events
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Your API key is in the Install panel of your funnel, under the "Server side" tab. It is a secret: keep it in your backend's environment, never in page HTML or client-side code. Regenerating it in the Install panel invalidates the old key immediately.

curl -X POST https://tiny-funnel.com/api/v1/events \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "app_installed",
"ip": "203.0.113.9",
"uid": "install-8271"
}'
Fields
| Field | Required | What it is |
|---|---|---|
name |
yes | The event name, 1 to 64 characters of letters, numbers, _, - and .. Case-sensitive, and it must match a Server event step's name exactly to count in the funnel. |
external_id |
one of these | Your own user id for the visitor, the same value your pages pass to tf.identify() or a tf-user meta tag. The strongest match: it follows the person, whichever browser or device they are in now. See the identity docs. |
visitor_token |
one of these | The visitor's Tiny Funnel id, read client-side from window.tf.visitor() and passed through your own stack. Exact, but tied to one browser. |
ip |
one of these | The original visitor's IP address, taken from the request your backend handled. Never your own server's address. |
user_agent |
no | The original visitor's browser user agent, from the same request you took the ip from. Used only to keep an IP match off the wrong device: a Windows conversion will never attach to an iPhone visitor. Send it whenever you send ip. |
uid |
recommended | Your own idempotency key for this event, up to 64 characters. Send the same uid on a retry and the event is only counted once. |
occurred_at |
no | ISO 8601 timestamp of when the event happened. Defaults to when it arrives. |
metadata |
no | A small JSON object of your own, shown with the event in the visitor's journey. |
Send every matcher you have. They are tried strongest first: external_id, then visitor_token, then ip.
How matching works
- By external_id: the event lands on the visitor your site identified with that id, whichever browser or device they are using now. Requires your pages to identify signed-in visitors — see the identity docs. And it works both ways: an event that matches by token or IP but carries an
external_idwrites that id onto the visitor, so your server events double as the most trustworthy identify there is. - By token: exact. The event lands on that visitor, in the one browser that minted the token.
- By IP: the event lands on the funnel's most recently seen visitor from that address within the last 30 days, ignoring bots. IPv6 addresses match on their /64 prefix, since devices rotate the low half daily. An address names a network, not a person (a household or office can share one), so if you also send
user_agent, candidates from a conflicting device family are skipped and a wrong-device match is refused outright: a missed match beats a wrong one. One warning from experience: if your marketing site and your app resolve differently (say one has IPv6 and the other does not), the same person can present a v6 address to one and a v4 address to the other, and those can never match. Give both properties the same connectivity, or carry a stronger matcher. - No match: the event is dropped, and recorded. The Install panel's server tab shows every unmatched event from the last 30 days, with which matchers it carried, so a misfiring integration is visible instead of silent.
Your visitor's raw IP is used once, in memory, to compute the same salted hash the script tag's hits are stored under. It is never written to disk, never logged and never stored.
IP matching is honest but not perfect. Shared office networks and mobile carriers can put many people behind one address, which is why the event attaches to the most recent visitor and why a stale address matches nothing. Where your stack can carry visitor_token end to end, prefer it.
Counting it in your funnel
Add a step of type Server event in the step editor and give it the event name, like app_installed. The step counts a visitor the moment an event with that exact name attaches to them. Per-step "when" conditions work like any other step. Events whose name matches no step still show up in the visitor's journey, which is a handy way to check an integration before wiring the step up.
Responses
| Status | Meaning |
|---|---|
202 |
Accepted and queued. The body carries the uid used, yours or a generated one. Matching happens asynchronously, so acceptance does not mean a visitor matched. |
401 |
Missing or invalid API key. |
403 |
The funnel is not currently ingesting (paused, or billing has lapsed). |
422 |
Missing or malformed name, or neither ip nor visitor_token given. |
429 |
Rate limit reached. Retry after a minute. |
Do it asynchronously
Send events from a background job or queue, never inline in a user-facing request. Your checkout should not wait on our API, and a network blip should not lose the event. The pattern that works:
- In the request handler, capture the visitor's IP (and
visitor_tokenif you carry it) and enqueue a job with a stableuid. - The job posts the event and treats any non-2xx as a logged failure, retried by your queue's own retry policy.
- Because the
uidrides along, a retried delivery is never double-counted.
Getting the visitor token
On any page running the Tiny Funnel script tag, window.tf.visitor() returns the visitor's id once tracking has booted. Put it in a hidden form field, a checkout attribute or a header to your own backend, store it against the user or order, and send it with the server event later. It outlives the session, so an event days after the visit still matches exactly.
Better still, if your visitors sign in: identify them once with tf.identify() (or a tf-user meta tag) and send external_id with your server events instead. The token names a browser; your user id names the person. The identity docs cover both, plus how to keep one journey across your domains and through emailed links.