Docs · Collect API

Send an event

Send events with a single HTTP request. POST a JSON payload with an event_name and your data becomes queryable within seconds. No SDK, no pipeline and no schema to register upfront.

POSThttps://useampersand.io/collectBearer auth · JSON body · 500 KB per request
Quick start

Send your first event

Nothing to install. Create an API key, send your first event and see it appear in the dashboard within seconds.

01
Create an API key

Settings → API keys → New API key. You use that key to authenticate your requests.

02
Send your first event

Copy the example below, replace the event_name and properties with your own and send the request.

03
Open the dashboard

Your event appears in Events within a few seconds. Once events start flowing, your dashboard panels will populate automatically.

shell
curl -X POST https://useampersand.io/collect \
  -H "Authorization: Bearer $AMP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "event_name": "page_view",
        "client_id": "usr_8f21c",
        "context": { "page": { "url": "https://example.com/pricing" } }
      }'
Response202 Accepted
HTTP/1.1 202 Accepted
content-length: 0
Reference

Properties

Only event_name is required. Every other field adds a dimension you can filter and break down by. Unknown keys are kept on the event and never reject it.

25 properties
Field
Core
event_namerequired
stringThe name of the event you want to track.
event_time
stringISO 8601 timestamp with an offset or Z. Defaults to the time the event was received. Values more than 48 hours in the future or 30 days in the past are clamped to the receipt time, with the value you sent kept in properties.
event_id
stringYour idempotency key, unique per project. Retry as often as you like — the same event is never counted twice. Defaults to a server-generated UUID.
Identity
client_id
stringThe field that connects events over time. Send a pseudonymous ID for each end user to turn individual events into visitors, sessions, and journeys. If you leave it out, the event still counts on its own. Never use the same value across users. A client that loads five or more pages within two seconds is counted as a bot.
session_id
stringUse only if you want to define sessions yourself. Otherwise, sessions are derived from client_id using a 30-minute inactivity window.
account_id
stringThe company, workspace or organization behind the user.
Classification
environment
stringprod by default. Use staging or dev for other environments. Any other value is stored, not rejected; the environment filter appears once a project has sent more than one.
platform
stringWhere the event came from: backend, web, mobile, worker or cli. An open string, not an enum.
level
stringEvent severity: info by default, or warn and error.
Metrics
value
numberThe amount that matters for this event. Never calculated from items — discounts, taxes and other adjustments make re-deriving the value unreliable.
currency
stringISO 4217 currency code. Only meaningful together with value. Mixed currencies are displayed separately and never summed.
duration_ms
numberDuration in milliseconds. Charted as p50 and p95 to make slow outliers visible instead of hiding them behind an average.
Errors
error.type
stringThe error type, such as TimeoutError or PaymentDeclined. This is the dimension used to group errors.
error.message
stringFree-form error details. Shown on every row rather than grouped, so IDs, hosts and other unique details remain useful.
Line items
items[]
arrayLine items containing sku, name, unit_price, quantity and an optional per-line currency. Visible on the event and filterable — never summed into value.
Context
context.page.url
stringThe full page URL. We extract the path and UTM parameters (utm_source, utm_medium and utm_campaign) from it. Send context.page.path yourself if your URLs carry IDs you would rather not store, and context.page.title to label the path.
context.page.referrer
stringThe referring URL. Used to determine source and medium when no UTM parameters are available.
context.client.user_agent
stringUsed to derive the device, operating system, browser and bot status. The raw value is evaluated once and then discarded — never stored, never logged.
context.client.ip
stringYour end user’s IP address, not your server’s. Used to derive continent, country, region and city, and to flag the event as a bot when the address belongs to a cloud or hosting provider such as AWS or Google Cloud — headless browsers often send an ordinary browser user agent. The raw value is evaluated once and then discarded — never stored, never logged.
context.client.is_bot
booleanYour own bot verdict, for example from Cloudflare, Vercel BotID or your WAF. It overrides our detection in either direction. True marks the event as a bot, while false counts it as a human even if the user agent or IP suggests otherwise. Only send false when your detection has actually checked the request. Leave the field out otherwise.
context.client.device_type
stringdesktop, mobile or tablet. When provided this value takes precedence over derived device information, and the same holds for context.client.os, os_version, browser, browser_version, language and timezone.
context.geo.country
stringISO 3166-1 alpha-2 country code such as DE. Populates the geographic breakdown without requiring an IP address, as do context.geo.continent, context.geo.region (ISO 3166-2) and context.geo.city.
context.app.version
stringYour application release version. Makes it easy to answer "Which deploy caused this?". context.app.name and context.app.build travel with it.
context.trace.request_id
stringYour own correlation ID. Use it to trace an event or error back to the corresponding logs. context.trace.parent_event_id chains an event to another one whose event_id you set yourself.
Your data
properties
objectProduct-specific data you want to track. Keys with a small, consistent set of values can become filters and breakdowns. Unknown top-level fields are merged in here; on a collision the explicit field wins. Limits: 100 keys, 128-character key names, 1024-character string values — exceeding one is a 400, not a silent drop.
Derived for you
context.client.user_agent→device_type · os · browser · bot detection
context.client.ip→continent · country · region · city · bot detection
context.page.url→path · utm_source · utm_medium · utm_campaign
context.page.referrer→utm_source · utm_medium
client_id→session_id (30-minute inactivity window)

context.client.user_agent and context.client.ip are derivation inputs only — evaluated once and never stored, logged or shared. Sending them means you are processing your end user’s IP address and user agent, so make sure you have a legal basis for that under the rules that apply to you.

Examples

Build your event

Pick the fields you want to send and add only the properties you need. You can start with event_name and add more context as your tracking evolves.

Preset
event_name
Core
Identity
Classification
Metrics
Errors
Context
Your data
4 fields · 222 bytes
{
  "event_name": "page_view",
  "client_id": "usr_8f21c",
  "context": {
    "page": {
      "url": "https://example.com/pricing",
      "referrer": "https://news.ycombinator.com/"
    },
    "client": {
      "ip": "203.0.113.7",
      "user_agent": "Mozilla/5.0 (Macintosh; …)"
    }
  }
}
This body unlocks
Unique visitors · SessionsPages · Sources · UTMDevices and OS
Crawlers

Track crawlers and AI bots

Because crawlers don't run JavaScript, browser-based tracking can't detect them. Send an event from your server instead, including the incoming request's user_agent and ip. Use Ampersand can then distinguish search engines, AI training crawlers, and AI assistants, and tell real crawlers from fake ones.

01
Include context.client.user_agent

Crawler classification relies on this value. Without it, the event is counted as a regular visit.

02
Include context.client.ip

We check crawlers that publish their IP ranges against them, so a fake Googlebot isn’t counted as one.

03
Leave out client_id

Crawler events won’t affect visitor or session counts when you leave out client_id.

04
Never make the crawler wait

Send the event in the background so the page can return right away.

05
Set environment

Events default to prod, so set the environment explicitly to keep crawlers on staging domains out of your production numbers.

Recommended: at the edge

Middleware at the edge runs before your CDN cache, so it can track crawler requests even when the CDN serves a page without sending it to your server.

middleware
// middleware.ts (in Next.js 16: proxy.ts, export function proxy)
import { NextResponse, type NextFetchEvent, type NextRequest } from "next/server";

const BOTS = /bot|crawl|spider|slurp|ChatGPT-User|Claude-User|Perplexity-User|meta-externalagent|anthropic-ai/i;

export function middleware(request: NextRequest, event: NextFetchEvent) {
  const userAgent = request.headers.get("user-agent") ?? "";
  const ip = request.headers.get("x-forwarded-for") ?? undefined;

  if (request.method === "GET" && BOTS.test(userAgent)) {
    event.waitUntil(
      fetch("https://useampersand.io/collect", {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${process.env.AMP_API_KEY}`,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          event_name: "page_view",
          environment: "prod",
          context: {
            page: { url: request.url },
            client: { user_agent: userAgent, ip }
          }
        })
      }).catch(() => {})
    );
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"]
};
Alternative: in your app

If you don't use a CDN, add the same check to your app's request handling. Pages served from cache never reach this code, so those crawler visits won't be recorded.

node 18+
// Express, before your routes
// Behind a proxy or load balancer: app.set("trust proxy", true), so req.ip is the visitor
const BOTS = /bot|crawl|spider|slurp|ChatGPT-User|Claude-User|Perplexity-User|meta-externalagent|anthropic-ai/i;

app.use((req, res, next) => {
  const userAgent = req.get("user-agent") ?? "";

  if (req.method === "GET" && BOTS.test(userAgent)) {
    fetch("https://useampersand.io/collect", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.AMP_API_KEY}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        event_name: "page_view",
        environment: "prod",
        context: {
          page: { url: `${req.protocol}://${req.get("host")}${req.originalUrl}` },
          client: { user_agent: userAgent, ip: req.ip }
        }
      })
    }).catch(() => {});
  }

  next();
});
What you see on the overview

Once crawler events arrive, the overview shows a Bot traffic card with the share of each group. Hover a segment for its requests and the change against the previous period.

AI answersAn assistant fetches your page because someone is asking it right now: ChatGPT-User, Claude-User, Perplexity-User.
IndexingSearch engines build their index: Googlebot, bingbot, Yahoo Slurp, DuckDuckBot, Applebot. The search crawlers behind ChatGPT, Perplexity and Claude count here too.
TrainingCrawlers collect pages to train AI models: GPTBot, ClaudeBot, CCBot, Bytespider, Meta-ExternalAgent.
Other botsRecognized as a bot, but none of the above: monitoring, SEO tools, scrapers and unclassified agents.
Status codes

Response codes

Send the same event_id twice and the second call is ignored, so retries are safe.

202Event accepted and queued for processing. There is no response body.
400Malformed JSON, a missing event_name, a request body over 500 KB, a batch over 500 events, or a properties limit exceeded. In a batch one invalid event fails the whole request — nothing lands partially.
401Missing, unknown or revoked API key. Check the Authorization header.
429Only past the grace threshold of 150% of your plan's event limit. Below it, events over the limit are still accepted and stored — you get a dashboard banner and an email instead of a rejection.
Pin a version

/collect is pinned to v1 and never moves to a new major version without notice. If you would rather control the version yourself, post to the versioned form instead: POST https://useampersand.io/api/v1/events. Both behave identically today.