Send events
Send a batch of events to MIRA FIVE with one HTTP request, from a server with the secret key or from a browser with the website key.
Events travel in batches: one JSON object with up to 1,000 events, sent with POST /v1/batch from a server (secret key as a bearer) or POST /v1/batch/{websiteKey} from a browser (website key in the path). Both take the same body and answer 202 with a receipt. Keys and CORS are on the Overview.
Send from a server
Put the secret key of a server source in MIRAFIVE_SECRET_KEY and send the batch as JSON:
curl -i https://events.mirafive.io/v1/batch \
-H "Authorization: Bearer $MIRAFIVE_SECRET_KEY" \
-H 'Content-Type: application/json' \
--data-binary @- <<EOF
{
"v": 1,
"batch": "$(uuidgen)",
"mode": "full",
"context": { "sdk": "acme-importer/1.0.0" },
"events": [
{
"name": "signup",
"time": $(date +%s)000,
"userId": "u_42",
"properties": { "plan": "pro" }
}
]
}
EOF{ "batch": "3b1f0e2a-8c4d-4e7f-9a6b-5c2d1e0f9a8b", "accepted": 1, "dropped": 0 }The shell fills in a new batch id and the current time in epoch milliseconds. Write a $ in an event name as \$ inside this heredoc ("\$identify"), or the shell expands it.
Send from a browser
Put the website key of a website source in the path and send the body as text/plain, so the browser sends no preflight:
curl -i https://events.mirafive.io/v1/batch/$MIRAFIVE_WEBSITE_KEY \
-H 'Content-Type: text/plain;charset=UTF-8' \
-H 'Origin: https://shop.example' \
-d '{"v":1,"batch":"0192d4a8-7b1c-4e8a-9c1d-2b3e4f5a6b7c","mode":"consentless","events":[{"name":"$pageview","page":{"url":"https://shop.example/pricing","title":"Pricing"}}]}'{ "batch": "0192d4a8-7b1c-4e8a-9c1d-2b3e4f5a6b7c", "accepted": 1, "dropped": 0 }https://shop.example stands for one of the source's allowed origins. In a page, navigator.sendBeacon() with a string body sends exactly this request, and keeps working while the tab closes:
const batch = {
v: 1,
batch: crypto.randomUUID(),
mode: 'consentless',
sentAt: Date.now(),
events: [
{
name: '$pageview',
time: Date.now(),
page: { url: location.origin + location.pathname, title: document.title, referrer: document.referrer },
},
],
}
navigator.sendBeacon('https://events.mirafive.io/v1/batch/mf_…', JSON.stringify(batch))A client you write for browsers sends nothing when navigator.doNotTrack is "1", navigator.globalPrivacyControl is true, the page is prerendering, or the host is localhost, 127.*, [::1], *.local or a file: URL. The browser SDK does all of this; use it where you can.
The batch
| Name | Type | Default | Description |
|---|---|---|---|
v | integer | — | Required. The protocol version, 1. |
batch | string | — | Required. A UUID (any version), new for every batch and kept for its retries. It is the idempotency key: see Idempotency. |
mode | string | — | Required. "consentless" or "full". See Collection modes. |
sentAt | integer | — | Epoch milliseconds when the request left the device. Stored with every event, and used as the time of events without time. |
context | object | — | Shared by every event of the batch. See Context. |
events | array | — | Required. 1 to 1,000 events. |
Integers must be JSON integers: "1727430000000" or 1727430000000.5 is refused. null in an optional field counts as absent. Fields the protocol does not define are ignored.
Events
| Name | Type | Default | Description |
|---|---|---|---|
name | string | — | Required. 1 to 128 characters, no leading or trailing whitespace. A leading $ is reserved: see Reserved event names. |
time | integer | sentAt, else the time received | Epoch milliseconds when it happened. A time more than 5 minutes ahead of the server or more than 30 days behind it is replaced by the time received. |
id | string | derived | A UUID, unique within the batch. Without one, the server derives a stable id from batch, the event's index and time, so a resent batch gets the same ids. |
page | object | — | url up to 2,048, title up to 512, referrer up to 2,048 characters. See Clean page URLs. |
properties | object | — | The event's data. See Properties. |
anonymousId | string | — | The browser or device, 1 to 256 characters, not blank. Full mode only. |
userId | string | — | Your id for the signed-in person, 1 to 256 characters, not blank. Full mode only. |
sessionId | string | — | A UUID for the visit. Full mode only. |
Lengths count Unicode code points; counting UTF-16 code units, as JavaScript does, is never more permissive. Send well-formed strings: a lone UTF-16 surrogate, for example from truncating an emoji in half, makes the whole body 400 invalid_json.
An anonymousId or userId that is a placeholder is stored as empty, so a broken integration cannot merge every visitor into one person. Placeholders are, compared after lower-casing ASCII letters and trimming spaces, tabs, line breaks and quotes: undefined, null, none, nan, 0, true, false, anonymous, guest, id, email, distinct_id, distinctid, not_authenticated, [object object].
Properties
properties is a JSON object. The server refuses the event's batch (400 validation_failed) when it:
- encodes to more than 32,768 bytes as UTF-8 JSON, counted without escaping
/or non-ASCII characters; - holds more than 64 leaf values (a list counts as one value, however long; an empty object counts as one);
- nests objects more than 5 levels deep;
- has a key longer than 128 characters.
Two properties have a meaning: revenue, a number, and currency, an ISO 4217 code such as "EUR". Revenue without a currency is counted in the project's reporting currency. See Track events.
Context
context describes the sender once for the whole batch:
| Name | Type | Default | Description |
|---|---|---|---|
sdk | string | — | name/version: name up to 64 characters without /, version up to 32 characters. For example acme-ruby/0.3.0. |
locale | string | — | BCP 47 language tag, 2 to 35 characters, e.g. de-DE. Full mode only. |
timezone | string | — | IANA time zone, up to 64 characters, e.g. Europe/Berlin. Full mode only. |
screen | array | — | [width, height], integers from 0 to 32,768. Full mode only. |
Device type, browser and location come from the request's User-Agent and address, only for website sources. Events from a server source carry no device and no location.
Collection modes
mode says what the batch may carry. The concepts are in Consent.
Consentless needs no consent banner and carries nothing that identifies a person or a device. A consentless batch MUST NOT carry:
anonymousId,userIdorsessionIdon any event;context.locale,context.timezoneorcontext.screen.
The server refuses the whole batch with 400 validation_failed rather than stripping the fields, so a misconfigured client is noticed. $identify, $search and $exposure events in a consentless batch are dropped, and the rest is kept. A client in consentless mode also reads no language, time zone or screen size and writes nothing to the device.
Full is for visitors who consented, or for servers acting on consent you already hold. It may carry identifiers and device context.
The source's mode is a ceiling. A full source accepts consentless batches (for example, before the visitor answers the banner). A consentless source refuses full batches with 400 collection_mode_not_allowed.
A request with Sec-GPC: 1 or DNT: 1 has its full batch stored as consentless: identifiers and ad click ids are not kept, and $identify, $search and $exposure are dropped.
Reserved event names
Names starting with $ belong to MIRA FIVE. Only these are accepted; any other $ name makes the batch 400 validation_failed.
| Name | Sent by | Carries |
|---|---|---|
$pageview | browser clients | page. Optional property $boot: 1 when the page's consent answer was known when it first drew. |
$autocapture | browser clients | Properties $event_type (click, submit or change), $el_tag, $el_selector, $el_id, $el_classes, $el_text, $el_href, $el_name, $el_type, $el_attrs. |
$identify | any client, full only | userId, and the person's traits as properties. See Identify users. |
$search | any client, full only | Property query. The server lower-cases it, replaces emails and runs of six or more digits, and cuts it to 100 UTF-16 code units. An event without a usable query is dropped. |
$exposure | any client, full only | Properties $experiment (^[a-z][a-z0-9-]{1,63}$), $variant (^[a-z][a-z0-9-]{0,39}$), optional $boot and $snippet; needs an anonymousId or a userId. An exposure missing any of these is dropped alone. See Exposures. |
$install_check | setup tools | Nothing. Proves a key and the host work; never stored or billed. |
An event named flagProperties is dropped: old trackers sent it by mistake.
Clean page URLs
The server stores a page URL as scheme, host, port and path only. From the query it keeps utm_source, utm_medium, utm_campaign, utm_term and utm_content, and the ad click ids gclid, gbraid, wbraid, fbclid, msclkid, ttclid and li_fat_id (the click id itself only in full mode). The referrer loses its query. A URL that is not http or https is not stored.
Clean URLs before they leave the device anyway, so nothing personal travels:
- Keep only query parameters whose name starts with
utm_or isref,source,gclid,gbraid,wbraid,fbclid,msclkid,ttclidorli_fat_id. Names compare case-sensitively; keep the kept parameters' original encoding. - Drop the fragment, unless the site routes by hash. Then keep it, and clean a query inside it the same way.
- A URL that does not parse loses everything from the first
?or#.
https://shop.example/pricing?utm_source=news&email=a%40b.example#plans becomes https://shop.example/pricing?utm_source=news.
The receipt
A batch the server has finished with answers 202 Accepted:
| Field | Type | Description |
|---|---|---|
batch | string | The batch id you sent. |
accepted | integer | Events kept, exposures included. |
dropped | integer | Events not kept. |
reason | string | Present only when nothing was kept for one of the reasons below. |
reason | Meaning |
|---|---|
bot | The User-Agent of a website batch belongs to a bot. |
install_check | The batch held only $install_check events. |
ingestion_paused | The organization is paused. |
allowance_exhausted | The plan's monthly events are used up. |
dropped above 0 without a reason means single events were dropped: person events in a consentless batch, a $search without a query, an invalid $exposure, or the part of a batch that crossed the monthly allowance (its first events are kept).
A 202 is final, whatever it says. Do not retry it. Every other status is described in Errors and retries.
Complete example
A full-mode batch from a browser after consent: a pageview, a click, an identify, an experiment exposure and a purchase.
{
"v": 1,
"batch": "6c1f0c2e-8f4a-4b7e-9a3d-5e2b1c0d9f8a",
"mode": "full",
"sentAt": 1727430060000,
"context": {
"sdk": "mirafive-browser/1.0.0",
"locale": "de-DE",
"timezone": "Europe/Berlin",
"screen": [1512, 982]
},
"events": [
{
"name": "$pageview",
"time": 1727430000000,
"page": { "url": "https://shop.example/checkout", "title": "Kasse", "referrer": "https://shop.example/pricing" },
"properties": { "$boot": 1 },
"anonymousId": "5f0c1c8e-3e0e-4a57-9d59-3f7f2a6d1e44",
"sessionId": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
},
{
"name": "$autocapture",
"time": 1727430012000,
"page": { "url": "https://shop.example/checkout" },
"properties": {
"$event_type": "click",
"$el_tag": "button",
"$el_selector": "form > button.primary",
"$el_text": "Jetzt kaufen"
},
"anonymousId": "5f0c1c8e-3e0e-4a57-9d59-3f7f2a6d1e44",
"sessionId": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
},
{
"name": "$identify",
"time": 1727430030000,
"properties": { "plan": "pro" },
"anonymousId": "5f0c1c8e-3e0e-4a57-9d59-3f7f2a6d1e44",
"userId": "u_42",
"sessionId": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
},
{
"name": "$exposure",
"time": 1727430000500,
"properties": { "$experiment": "hero", "$variant": "b", "$boot": 1, "$snippet": "fa1adf55" },
"anonymousId": "5f0c1c8e-3e0e-4a57-9d59-3f7f2a6d1e44",
"userId": "u_42",
"sessionId": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
},
{
"name": "order completed",
"time": 1727430055000,
"page": { "url": "https://shop.example/thanks" },
"properties": { "revenue": 49.9, "currency": "EUR", "items": [{ "sku": "tee-black", "quantity": 2 }] },
"anonymousId": "5f0c1c8e-3e0e-4a57-9d59-3f7f2a6d1e44",
"userId": "u_42",
"sessionId": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
}
]
}{ "batch": "6c1f0c2e-8f4a-4b7e-9a3d-5e2b1c0d9f8a", "accepted": 5, "dropped": 0 }JSON Schema
JSON Schemas (draft 2020-12) describe what a client sends and receives. Validate your batches against them in tests:
| Schema | Describes |
|---|---|
batch.schema.json | the batch, including the consentless rules |
receipt.schema.json | the 202 receipt |
error.schema.json | the error body |
A batch that passes the schema can still be refused for what a schema cannot express: event ids repeated within the batch, properties over 32,768 bytes, 64 leaf values or 5 levels, and a body over 1 MiB.