> ## Documentation Index
> Fetch the complete documentation index at: https://docs.davazmysel.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Server-Side Events

> Send backend-confirmed events — payments, policy purchases, CRM updates — into a visitor's existing journey.

The browser snippet already captures pageviews, clicks, and form submits automatically. Some facts are only known on your backend — a payment cleared, a policy was issued, a refund was processed. The server-side events API lets your backend send those facts directly into Dáva, joined onto the same journey the visitor's browser started.

<Note>
  This is additive, not a replacement. Server-side events **enrich** an existing journey — they never create one. A journey always starts with a browser-attributed entry (via the [tracking snippet](/tracking/install-snippet)); the server confirms what happened on it.
</Note>

## Authentication

Every request needs a Bearer API key. Create one under **Settings → Workspace → Settings & Integrations** (API Keys card) — copy the plaintext value immediately, it is shown once. Use a `Test` key while integrating and switch to `Live` for production traffic.

```
Authorization: Bearer dava_sk_live_...
```

## Joining the visitor's journey

Pass `dava_session_id` — the same session ID the browser snippet is already using — so the event lands on the visitor's existing journey instead of standing alone.

```js theme={null}
// In the browser, after the snippet has loaded:
const davaSessionId = window.dava.sessionId; // UUID, or null before the first session exists
```

How you get that value from the browser to your backend is up to you (hidden form field, cookie, custom header, or your own API call). Then include it as `dava_session_id` in the event body.

<Warning>
  If `dava_session_id` is missing or does not match a session in your workspace, the event is still stored — but only as an **Unmatched Server Event**, kept for diagnostics and never attributed to a journey. Check the `session_match` field in the response ("matched" or "unmatched") to confirm your integration is threading the session ID correctly.
</Warning>

## Identity

At least one of `anonymous_id`, `contact_id`, or `hashed_email` is required — the request is rejected with `400` otherwise. `dava_session_id` handles journey attribution; these fields drive identity-cluster matching (linking the same person across visits/devices), a separate concern.

## Sending a single event

```http theme={null}
POST https://api-v2.davazmysel.com/v1/events
Authorization: Bearer dava_sk_live_xxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

{
  "subdomain": "checkout.yourcompany.com",
  "event_type": "conversion",
  "event_name": "policy_created",
  "dava_session_id": "01941b8c-3ae4-7e2a-9b5a-4e2a88c60d10",
  "anonymous_id": "anon-id-from-dava-cookie",
  "event_id": "policy_POL123",
  "transaction_id": "POL123",
  "value": 149.00,
  "currency": "AUD"
}
```

On a **custom-domain install** (your own hostname configured per [tracking snippet installation](/tracking/install-snippet)), omit `subdomain` — Dáva resolves the domain from the request's `Host` header instead.

### Request fields

| Field                                | Type              | Required           | Notes                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ------------------------------------ | ----------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `subdomain`                          | string            | Conditional        | Required when calling `api-v2.davazmysel.com` directly; omit (or send anything — it's ignored) on a custom-domain install.                                                                                                                                                                                                                                                                                                        |
| `anonymous_id`                       | string            | One of these three | Matches the snippet's anonymous ID cookie, if you have it.                                                                                                                                                                                                                                                                                                                                                                        |
| `contact_id`                         | string            | One of these three | Your own customer/contact ID.                                                                                                                                                                                                                                                                                                                                                                                                     |
| `hashed_email`                       | string            | One of these three | SHA-256 hex, with or without a `sha256:` prefix.                                                                                                                                                                                                                                                                                                                                                                                  |
| `event_type`                         | enum              | Yes                | One of `pageview`, `form_submit`, `click`, `conversion`, `send`, `custom_event`, `contact_click`, `outbound_click`, `scroll`, `file_download`. This is a fixed classification, not a free-form label — a purchase or policy issuance is `conversion`, with the specific name in `event_name`. Dáva does not decide whether an event is a Hot Lead or Acquisition here; your [Goal Tracking rules](/tracking/funnel-and-goals) do. |
| `event_name`                         | string            | No                 | Free-form label, e.g. `"policy_purchased"`. Max 256 chars.                                                                                                                                                                                                                                                                                                                                                                        |
| `dava_session_id`                    | UUID              | No                 | The journey to join. See [Joining the visitor's journey](#joining-the-visitor-s-journey) above.                                                                                                                                                                                                                                                                                                                                   |
| `event_id`                           | string            | No                 | Your stable per-event dedup key. Max 128 chars. **Priority 1** in dedup — wins over `transaction_id`, `properties` IDs, and `external_id` when all are present. Any opaque string (not a UUID). E.g. `"policy_POL123"`. **Distinct from the response `event_id`**, which is Dáva's internally-assigned UUID. See [Deduplication](#deduplication) below.                                                                           |
| `transaction_id`                     | string            | No                 | The business transaction this event belongs to. Max 128 chars. **Priority 2** in dedup. E.g. `"POL123"`. Convenience alias for `properties.transaction_id` — whichever you send is equivalent, but the top-level field takes priority when both are present.                                                                                                                                                                      |
| `external_id`                        | string            | No                 | Safe-retry idempotency key. **Priority 4** in dedup — used only when `event_id`, `transaction_id`, and `properties` dedup fields are all absent. See [Deduplication](#deduplication) below.                                                                                                                                                                                                                                       |
| `url_path` / `full_url` / `referrer` | string            | No                 | URL context, if relevant to a backend-originated event.                                                                                                                                                                                                                                                                                                                                                                           |
| `value`                              | number            | No                 | Non-negative.                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `currency`                           | string            | No                 | 3-letter ISO 4217 code, e.g. `AUD`.                                                                                                                                                                                                                                                                                                                                                                                               |
| `properties`                         | object            | No                 | Any additional facts — passed through as-is, **except** `transaction_id`, `quote_id`, `policy_id`, and `order_id`, which double as **priority-3** dedup keys (see [Deduplication](#deduplication)).                                                                                                                                                                                                                               |
| `occurred_at`                        | ISO 8601 datetime | No                 | When the event actually happened (e.g. the payment confirmation timestamp from your payment provider). Defaults to the time Dáva receives the request.                                                                                                                                                                                                                                                                            |
| `adfixus_id`                         | string            | No                 | Adfixus identity signal, if you use it. Enrichment only — does not affect identity matching or session join.                                                                                                                                                                                                                                                                                                                      |

<Note>
  `source` is **not** a request field. Dáva derives it internally from the ingestion path (`"api"` for all `/v1/events` deliveries). Any `source` value sent in the request body is silently ignored.
</Note>

### Deduplication

Duplicates at the delivery layer are expected — a network retry, a queue redelivery, or your own backend firing the same webhook handler twice. Dáva stores every delivery it receives for the same underlying action, but reports it as **one canonical event**.

Two events are treated as the same action when they share a dedup key, checked in this order:

1. `event_id` (top-level) — your stable per-event dedup key. E.g. `"policy_POL123"`. Any opaque string up to 128 characters — no UUID constraint.
2. `transaction_id` (top-level) — the business transaction this event belongs to. E.g. `"POL123"`. Takes priority over the same value embedded in `properties`.
3. `properties.transaction_id` — or one of its aliases `quote_id`, `policy_id`, `order_id`. Use whichever name matches your domain (an insurer sends `policy_id`, an e-commerce store sends `order_id`).
4. `external_id` — your safe-retry idempotency key, if none of the above is present.

If none is present, the event is stored as-is with no dedup applied — every request creates a new event.

**Backward-compatible:** existing callers experience no change. A caller sending only `external_id` still deduplicates at priority 4. A caller sending only `properties.transaction_id` still deduplicates at priority 3. Both orderings are preserved exactly.

When a second delivery matches an existing key:

* The response is `200` (not `201`) with `deduped: true` and the **same** `event_id` as the first delivery.
* Any field you send with a non-null value (`value`, `currency`, `properties`) **updates** the stored event — a `null`/absent field never erases a value that was already there. This lets a browser-side event with no `value` get enriched by a later server confirmation carrying the real amount, without one delivery overwriting the other's data.
* `created_at` in the response reflects the *original* delivery's timestamp, not the retry.

<Tip>
  For cross-path dedup (a browser-snippet event and a server-side confirmation of the same purchase), use a shared business ID: supply it as top-level `transaction_id` from your server and as `properties.order_id` (or `quote_id`/`policy_id`) from the browser side — both resolve to the same dedup key. Top-level `event_id` is ideal when your system already assigns a stable per-event ID (e.g. a policy number prefixed with `policy_`, giving `"policy_POL123"`).
</Tip>

### Response

```json theme={null}
{
  "event_id": "01941b8c-3ae4-7e2a-9b5a-000000000001",
  "deduped": false,
  "created_at": "2026-07-18T09:23:00.000Z",
  "session_match": "matched",
  "dava_session_id": "01941b8c-3ae4-7e2a-9b5a-4e2a88c60d10"
}
```

`201` on first receipt, `200` if the event deduplicated against a prior delivery (`deduped: true`) — see [Deduplication](#deduplication) above.

| Field             | Meaning                                                                                                                                                                                                                                   |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `event_id`        | Dáva's internally-assigned event UUID. **Not** the `event_id` you may supply in the request — that is your own dedup key (an opaque string). The response `event_id` is always a UUID, regardless of what you sent as request `event_id`. |
| `deduped`         | `true` if this delivery's dedup key (`event_id` → `transaction_id` → `properties.transaction_id`/`order_id`/`quote_id`/`policy_id` → `external_id`, whichever was present first) matched a prior delivery for your account.               |
| `session_match`   | `"matched"` or `"unmatched"` — use this during integration testing.                                                                                                                                                                       |
| `dava_session_id` | Echoes your input when matched; `null` when unmatched.                                                                                                                                                                                    |

## Sending multiple events

```http theme={null}
POST https://api-v2.davazmysel.com/v1/events/batch
Authorization: Bearer dava_sk_live_xxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

{
  "events": [
    { "subdomain": "checkout.yourcompany.com", "event_type": "conversion", "anonymous_id": "...", "dava_session_id": "..." },
    { "subdomain": "checkout.yourcompany.com", "event_type": "custom_event", "event_name": "renewal", "contact_id": "...", "external_id": "renewal_456" }
  ]
}
```

Up to 1000 events per request, processed asynchronously — each event is independent, so one invalid event in the batch does not fail the others.

```json theme={null}
{ "job_id": "b3f1...", "status": "queued", "event_count": 2, "processed_count": 0 }
```

Poll the job with `GET /v1/events/batch/{job_id}`:

```json theme={null}
{
  "job_id": "b3f1...",
  "status": "completed",
  "event_count": 2,
  "processed_count": 2,
  "failed_count": 0,
  "errors": []
}
```

## Errors

| Status | Error                          | Cause                                                                                                                                               |
| ------ | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Validation error               | Missing `event_type`, no identity signal present, malformed `dava_session_id` (not a UUID), invalid `hashed_email`/`currency`/`occurred_at` format. |
| `401`  | Unauthorized                   | API key missing, malformed (`Bearer` prefix required), or revoked.                                                                                  |
| `403`  | `DOMAIN_SCOPE_VIOLATION`       | The key is scoped to a domain and `subdomain` falls outside that domain's family.                                                                   |
| `422`  | `DOMAIN_NOT_FOUND`             | `subdomain` is not registered to this account.                                                                                                      |
| `429`  | Too Many Requests (batch only) | Rate limit exceeded — `Retry-After` header tells you how long to wait.                                                                              |
