VisitTrack
Start free
☰ Browse the docs

Developers

Send events from your server

For the events that never touch a browser — a payment webhook, a background job, anything the tracking script structurally can't see.

tracker.js can only report what happens in a browser tab. A lot of the events that actually matter for a funnel don't: 'payment succeeded' is a Stripe webhook, 'video finished processing' is a background worker, 'account upgraded' might be a cron job reconciling a subscription. POST /api/track lets your backend write those events (and identify() calls) directly, using the same visitorId your pages already send.

Not the same key as tracker.js

The script tag uses your public data-site id — fine to expose, it can only ever write pageviews/clicks from wherever it's embedded. /api/track is authenticated like the read API instead: a secret key from Settings → API / MCP, sent as Authorization: Bearer <key>, and never put in client-side code.

Send an event

code
curl -X POST https://visitrack.app/api/track \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "event",
    "visitorId": "the_ana_vid_value_from_checkout_metadata",
    "name": "checkout_completed",
    "props": { "plan": "pro", "amountCents": 4900 }
  }'

visitorId is the same value tracker.js already persists in localStorage's _ana_vid and that you're (hopefully) already passing through to Stripe as client_reference_id or similar — that's what lets a server-originated event join up with everything the visitor did in the browser before it. name becomes a Funnel step exactly like a client-side custom event does. props is an optional object, capped at 4KB — past that it's dropped, not rejected, so a request with a huge props value still records the event.

If this visitorId has never been seen before, the visitor is created rather than the request being rejected — a webhook can genuinely be the first thing you ever hear about someone (say, their signup event lagged your queue). There's one honest gap: a visitor created this way has no referrer or UTM to stamp, so its first-touch fields stay null, same as they would for a visitor whose real first pageview simply hasn't arrived yet.

identify() from the server

Same call the client tracker exposes as window.identify(externalId, traits), for the case where the link between an anonymous visitor and your own user id is only known server-side (an OAuth callback, an invite acceptance, anything that doesn't run identify() in the browser at the moment it happens):

code
curl -X POST https://visitrack.app/api/track \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "identify",
    "visitorId": "the_ana_vid_value",
    "externalId": "usr_8f2a1",
    "traits": { "plan": "pro", "email": "founder@example.com" }
  }'

traits merge into whatever's already stored — a second call with just {"plan": "pro"} doesn't erase traits an earlier call set. Unlike the event call above, identify() from the server requires a visitorId that's already known: there's no page navigation behind a bare identify(), so there's nothing to safely create a Visitor row from. An unknown visitorId answers 400 rather than silently creating one.

payment_completed happens automatically

If you're using VisitTrack's built-in revenue attribution (Stripe, Polar, Lemon Squeezy, Paddle or Razorpay — see Settings → Revenue), you don't need to call /api/track for the payment itself. Every successful charge that resolves to a known visitor already writes a payment_completed event with amountCents/currency/provider in props, alongside the Conversion row your Revenue tab reads — so checkout_started → payment_completed is a Funnel step with zero integration work. Sending it yourself as well would just double it up.

Rate limit and errors

  • 300 requests per 10 seconds per key — generous, since a real integration can legitimately burst (a backfill, a queue catching up), but still enough to catch a retry loop that never marks its job done.
  • Auth errors, and their JSON shape, are identical to the read API's — see the reference page's Errors section.
  • A successful call answers 200 with { "ok": true }, plus eventId and visitorId for type: "event".

Something missing? Tell us.

AI agent or LLM? Read this page as markdown.