# Sending data


The three ingestion endpoints are the same for every client. What differs in a browser is how you reach them: which key you may use, why a request can be rejected on its origin, how to write a timestamp in JavaScript, and how to get the last batch out while the page is closing.

For the endpoints themselves, see [Errors payload](/docs/protocol/errors/payload), [Traces payload](/docs/protocol/traces/payload) and [Logs payload](/docs/protocol/logs/payload).

## Use the public project key

A browser client authenticates with your project's **public** API key, in the `x-api-token` header, on all three endpoints.

Never ship the private key. Everything in a bundle can be read by anyone who opens the page, so a private key in front-end code is a leaked private key. The public key is built for this: it can send telemetry and upload sourcemaps, and nothing else.

The [sourcemap endpoint](/docs/protocol-javascript/errors/sourcemaps) also takes the public key, but reads it from a `key` field in the body instead of from a header.

## The origin allowlist

Because a public key travels with your bundle, a project can list the domains it accepts browser traffic from. Flare checks that list at the edge, before anything else happens to your payload.

- The check reads the `Origin` header, and falls back to the hostname of `Referer` when `Origin` is absent.
- When neither yields a hostname, or the hostname is not on the list, the request is rejected with `403` and the message `Origin is not allowed for this public key.`
- An empty list allows every origin. That is the default, so a new project sends without configuring anything.
- Requests made with the private key skip the check, so server-side code is never affected by it.

A pattern may contain `*`, as in `*.example.com`. A pattern without a `*` has to equal the hostname, so `example.com` does not match `app.example.com`.

Changes to the list need up to about 90 seconds to reach the edge. Someone who corrects their allowlist and immediately retries still gets the `403`.

### The rejection is a real response, not a CORS failure

Preflight requests always answer with `Access-Control-Allow-Origin: *`, including for an origin that is not allowed. The rejection then arrives as an ordinary `403` that your client can read, log and report on. Rejecting at preflight instead would surface in the browser as an opaque CORS error, which tells you nothing about what went wrong.

## Writing timestamps in JavaScript

The traces endpoint requires JSON integers. `startTimeUnixNano`, `endTimeUnixNano` and a span event's `timeUnixNano` are rejected with a validation error when you send them as strings.

The logs endpoint is looser: `timeUnixNano` and `observedTimeUnixNano` accept an integer or a string.

Two things follow from that in JavaScript.

**Round before you send.** `Date.now()` and `performance.now()` both give milliseconds, so you multiply by one million to get nanoseconds. That multiplication can leave a fraction, and a fractional value is not an integer, so the traces endpoint rejects it. Round the result.

**Expect the last few digits to move.** A nanosecond timestamp is around `1.7e18`, well past `Number.MAX_SAFE_INTEGER`. A JavaScript number still holds a value that size, but only to roughly 256 nanoseconds of resolution. That is far finer than anything a browser clock measures, so no real timing information is lost. It does mean the number is not an exact identifier: never use a timestamp to tell two spans apart, use the span id. Copying a timestamp from one span to another is fine, as [web vitals](/docs/protocol-javascript/traces/web-vitals) requires, because the same number goes out both times.

## Getting the last batch out

Sending one request per span would be wasteful, so spans and log records are collected and sent in batches. The batch that is still buffered when the visitor leaves is the one that needs care.

Start the final flush when the document becomes hidden, through `visibilitychange`. `unload` and `beforeunload` do not fire reliably, in particular on mobile, so a client that waits for them loses the tail of most sessions.

A request started at that moment is cancelled along with the document unless it is marked to outlive it. Use `fetch` with `keepalive`, or `navigator.sendBeacon`. Browsers cap the combined body of all in-flight keepalive requests at roughly 64 KB, and a request over that budget fails outright. On this path, send the newest records that fit and drop the rest, rather than losing the whole batch.

This is also when the second web vitals request goes out. Send it on the first client-side navigation or on page hide, whichever comes first. See [Web vitals](/docs/protocol-javascript/traces/web-vitals).

## Sampling

Sampling is a decision the client makes, and it leaves no trace in the payload. There is no field that tells Flare a trace stands for five others, so the aggregate numbers count what arrived.

Take the decision once, when the root span of a trace is created, and apply it to every span in that trace. A trace must be sent whole or not at all. Sending some spans and holding others back breaks the `parentSpanId` chain, and the spans that did arrive get dropped during processing without any signal that it happened. See [Lifecycle](/docs/protocol-javascript/traces/lifecycle#parent-references-across-requests).
