Events API

Send your product's events to Mara. One endpoint, two operations: identify tells Mara who a contact is, track tells her what happened. Any active journey whose trigger matches an incoming event fires automatically, per contact.

This page is the complete spec, written for humans and for AI coding agents. The raw markdown lives at hiremara.com/docs/events-api.md. The fastest path: open Settings, then Integrations, then Events API in your dashboard and copy the ready-made prompt into Claude Code, Cursor, or whatever agent writes your code. It links back here.

If your "events" are billing events from Stripe or Polar, do not send them through this API. Point your provider's webhooks at Mara instead, so payloads arrive signature-verified and normalized. See Webhooks.

Endpoint

POST https://hiremara.com/api/tenants/{tenantId}/events
Content-Type: application/json
Authorization: Bearer {your ingestion key}

Your exact endpoint URL, with your tenant id filled in, is shown in Settings under Integrations, Events API.

Authentication

Auth is a per-tenant bearer key with the prefix mara_ing_. Generate it on the same settings row. The raw key is shown exactly once at generation; Mara stores only a hash. Generating a new key immediately revokes the old one.

This is a server-to-server key. Keep it in an environment variable on your backend. Never ship it to a browser or a mobile client.

Request body

The body is a single operation, or a batch of up to 100:

{
  "batch": [
    {
      "type": "identify",
      "userId": "u_42",
      "email": "[email protected]",
      "name": "Ada",
      "traits": { "plan": "starter" }
    },
    { "type": "track", "event": "user.signed_up", "userId": "u_42", "messageId": "evt_001" }
  ]
}

identify

Upserts the contact. Pure projection: it never writes an event and never fires a journey.

FieldTypeRequiredNotes
type"identify"yes
userIdstringat least one of userId, externalId, emailYour stable user id.
externalIdstringSame meaning as userId. If both are sent, externalId wins.
emailstringMust be a valid email address.
namestringnoDisplay name.
traitsobjectnoCustom attributes, flat scalars only. See sanitization below.

track

Records an event against a contact and fires any matching journeys.

FieldTypeRequiredNotes
type"track"yes
eventstringyesThe event type, 1 to 120 characters. See naming below.
userId / externalId / emailstringsend at least oneSame identity rules as identify. A track with no identity is skipped, not an error.
propertiesobjectnoEvent details, flat scalars only. Same sanitization as traits.
timestampstringnoISO 8601. Defaults to arrival time. Invalid values fall back to arrival time.
messageIdstringnoYour idempotency key for this event. Strongly recommended.

Identity resolution

externalId (or userId) is the contact's primary identity; email matches or creates the contact when no external id is sent. Send the same userId from signup onward and Mara keeps one contact per user, even if the email changes later. Contacts that arrive with an email get lifecycle consent recorded automatically, so journey sends are not blocked downstream.

Traits and properties sanitization

traits and properties become contact attributes and event details that Mara's agents can reference in copy, so they are filtered at the door:

Idempotency and retries

Set messageId on every track op: a UUID, or the id of the row in your own event log. Retrying a request with the same messageId is safe; the duplicate op returns "status": "duplicate" and writes nothing. Identify ops are upserts and naturally safe to retry.

Rate limits

600 requests per hour per tenant, token bucket, burst up to 600. A batch of 100 operations counts as one request, so batching gives you up to 60,000 events per hour. Over the limit you get a 429 with a Retry-After header in seconds. Back off and retry after that.

Responses

Success is a 200 with one result per operation, in order:

{
  "ok": true,
  "results": [
    { "index": 0, "type": "identify", "status": "ok", "contactId": "9a1f..." },
    { "index": 1, "type": "track", "status": "ok", "contactId": "9a1f..." }
  ]
}

Per-op status values: ok, skipped_no_identity (the op carried no usable identity), duplicate (the messageId was already processed).

Errors return { "ok": false, "code": "...", "message": "..." }:

HTTPcodeMeaning
400bad_requestBody is not valid JSON or fails the schema.
401unauthorizedMissing or wrong bearer key.
412no_ingestion_keyNo key has been generated for this tenant yet.
429rate_limitedOver the hourly cap. Honor Retry-After.
500infraSomething broke on Mara's side. Safe to retry with the same messageIds.

How events fire journeys

Event types are free-form. Two ways they connect to journeys:

  1. The signup event. On the same settings row, tell Mara which event means "a new user signed up" (for example user.signed_up). Saving it seeds a draft welcome journey that fires on that event; you approve the draft from your dashboard to make it live.
  2. Any other event. Ask Mara in chat. "When a user fires project.created, start the activation journey" is enough; she drafts the journey with an on-event trigger and you approve it.

Naming guidance: lowercase, dot-namespaced, object.action. user.signed_up, project.created, report.exported. Allowed characters are letters, digits, spaces, ., _, :, -, /, up to 120 characters. Keep types low-cardinality: no user ids or timestamps inside the type string. Details belong in properties.

Quickstart

Test with curl (the endpoint URL and key come from your settings page):

curl -X POST "https://hiremara.com/api/tenants/{tenantId}/events" \
  -H "Authorization: Bearer $MARA_INGESTION_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "batch": [
      { "type": "identify", "userId": "u_42", "email": "[email protected]", "name": "Ada" },
      { "type": "track", "event": "user.signed_up", "userId": "u_42", "messageId": "test-001" }
    ]
  }'

A minimal Node helper:

const MARA_EVENTS_URL = process.env.MARA_EVENTS_URL; // from your settings page
const MARA_INGESTION_KEY = process.env.MARA_INGESTION_KEY;

export async function maraSend(ops) {
  const res = await fetch(MARA_EVENTS_URL, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${MARA_INGESTION_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ batch: ops }),
  });
  if (!res.ok) throw new Error(`Mara events API ${res.status}: ${await res.text()}`);
  return res.json();
}

// At signup:
await maraSend([
  { type: 'identify', userId: user.id, email: user.email, name: user.name },
  { type: 'track', event: 'user.signed_up', userId: user.id, messageId: `signup-${user.id}` },
]);

Call Mara off the request path (a queue, a background job, or at minimum fire-and-forget with a catch). An email platform should never be able to slow your product down.

Verify it landed

Three checks, in order:

  1. The HTTP response: 200 with "status": "ok" per op.
  2. Settings, Integrations: the ingestion health strip shows per-connector contact counts and last activity.
  3. Ask Mara in chat: "did you get my test event?"

For AI coding agents

If you are an agent implementing this integration, the checklist:

  1. Put the endpoint URL and ingestion key in env vars (MARA_EVENTS_URL, MARA_INGESTION_KEY). Never hardcode the key; never expose it client-side.
  2. Add a small server-side helper that POSTs { batch: [...] } with the bearer header. Batch when you can, up to 100 ops.
  3. On signup: send one identify (userId, email, name, useful traits) plus one track with the agreed signup event type.
  4. Instrument the 3 to 5 lifecycle moments that matter (activation milestone, key feature used, plan limit reached). Stable dot-namespaced event types, details in properties.
  5. Set a deterministic messageId per event so retries are safe. Retry on 429 (honor Retry-After) and on 5xx; do not retry 4xx.
  6. Keep Mara off the hot path: queue or fire-and-forget. A Mara outage must not break signup.
  7. Verify with the curl above, then confirm in the response that every op returned "status": "ok".