FirstMRRHelp Center
Developers

Server events API

Send your conversions from your backend with a secret key: schema, responses, errors, idempotency and limits.

4 min read

Endpoint

POSThttps://firstmrr.app/api/v1/eventsAuthorization: Bearer sk_live_…

A JSON body { "events": [...] } with 1 to 100 events, 64 KB at most. Each event is accepted or refused on its own: one invalid event never blocks the others.

curl https://firstmrr.app/api/v1/events \
  -H "Authorization: Bearer $FIRSTMRR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"events":[{"type":"signup","event_id":"user_8412","visitor_id":"6f1c2b9e-1d4a-4c55-9a3e-2b7d0c9e8f11","source":{"method":"link","ref":"tiktok-oscar-3f9a"}},{"type":"payment","event_id":"in_1PqXyZ","value_cents":1999,"currency":"EUR","visitor_id":"6f1c2b9e-1d4a-4c55-9a3e-2b7d0c9e8f11","variants":["pricing_page:b"]}]}'

An event

FieldTypeRequiredDescription
typestringRequiredOne of signup, activation, trial, payment, cancel, source, exposure, custom.
event_idstringRequiredYour unique id for the event (1 to 64 characters: letters, digits, _ . : -). The same id is only counted once: it is the idempotency key. For a payment, use the invoice or order id, the same as in track("payment", { id }).
occurred_atstringOptionalISO 8601 date with a time zone (2026-10-12T14:03:00Z), within the last 72 hours. Defaults to the time of receipt.
visitor_idstringOptionalThe value of firstmrr.visitorId() in the browser, or your app's install id (8 to 64 letters, digits or dashes). Ties the event to the same day's visitor.
value_centsintegerOptionalPayment only: the amount in cents (1999 for 19.99).
currencystringOptional3-letter ISO 4217 code, required with value_cents.
sourceobjectOptionalThe origin returned by firstmrr.source() at signup, as is: method among link, utm, in_app, referrer, with platform or ref (at is accepted and ignored).
variantsstring[]OptionalThe visitor's A/B assignments, "test:variant", 4 at most. Required for exposure.
namestringOptionalcustom events only: its name, 1 to 40 lower-case letters, digits or _.
answerstringOptionalsource events only: the answer to "Where did you hear about us?", among tiktok, instagram, youtube, x, threads, facebook, linkedin, snapchat, pinterest, google, app_store, friend, press, other.
accountstringOptionalsource events only: the account the person names ("@account"), 60 characters at most. An e-mail address is refused.

Response

202 Accepted once the request is read: accepted counts the stored events, duplicates those already received with the same event_id, and rejected gives, for each refusal, its position in your request, a code and the field at fault.

json
{
  "accepted": 1,
  "duplicates": 1,
  "rejected": [
    {
      "index": 2,
      "error": "unknown_field",
      "field": "email",
      "message": "This field is not part of the schema (personal data such as e-mails or names is never accepted)."
    }
  ]
}

Errors

A request error refuses the whole request, with { "error": { "code", "message" } }:

CodeHTTPDescription
missing_key401Send your secret key in the Authorization header: Bearer sk_live_…
invalid_key401This secret key does not exist or was revoked.
invalid_json400The body is not valid JSON.
body_too_large400The body is larger than 64 KB.
invalid_body400The body must be an object with an events array, and nothing else.
empty_batch400The events array is empty.
too_many_events400Send 100 events per request at most.
rate_limited429Too many requests: 600 per 15 minutes per account. Retry after the Retry-After delay.
unavailable503The events API is momentarily unavailable. Retry later.

An event error only refuses that event, in rejected:

CodeScopeDescription
invalid_eventeventThe event must be an object.
unknown_fieldeventThis field is not part of the schema (personal data such as e-mails or names is never accepted).
invalid_typeeventtype must be one of signup, activation, trial, payment, cancel, source, exposure, custom.
invalid_event_ideventevent_id is required: 1 to 64 letters, digits, _ . : or -.
invalid_occurred_ateventoccurred_at must be an ISO 8601 date with a time zone, within the last 72 hours.
invalid_visitor_ideventvisitor_id must be 8 to 64 letters, digits or dashes (firstmrr.visitorId() or the app's install id).
invalid_valueeventvalue_cents must be a whole number of cents, from 0 to 9 999 999, on a payment only.
invalid_currencyeventcurrency must be a 3-letter ISO 4217 code, given with value_cents.
invalid_sourceeventsource must be an object with method among link, utm, in_app, referrer and its platform or ref.
invalid_variantseventvariants must be a list of 1 to 4 "test:variant" strings, one per test.
invalid_nameeventname is required on a custom event, only there: 1 to 40 lower-case letters, digits or _.
invalid_answereventanswer is required on a source event, only there: one of tiktok, instagram, youtube, x, threads, facebook, linkedin, snapchat, pinterest, google, app_store, friend, press, other.
invalid_accounteventaccount (a source event only) must be a social handle or a name of 60 characters at most, never an e-mail.
daily_capeventThe site reached its daily event cap: retry after 00:00 UTC.

Idempotency and retries

An event_id already received for the same event type is never counted twice, whether it came from the API or the script (track("payment", { id })). After a network error, a 503 or a 429, resend the same batch safely: events already stored come back as duplicates.

Rate limits

  • 600 requests per 15-minute window across all your keys; beyond that, 429 with a Retry-After header (in seconds).
  • 100 events per request: batch rather than sending one call per event.
  • 3,000 events per day per site or app, script included; beyond that, events come back as daily_cap until midnight UTC.

Tying a server event to its visitor

Without visitor_id, the event is counted on its own. With the value of firstmrr.visitorId() (site) or the install id (app), it joins the same day's visitor: their visits, origin and A/B variants. Also pass source (the origin saved at signup) and variants when you have them: they are what ties a server payment to its channel and its variant.