# API conventions

A server-to-server JSON API for reading records, sharing them, and receiving records shared with you.
Everything here applies to every endpoint.

```bash
curl -sS \
  -H "X-API-Key: $EC_API_KEY" \
  "https://api-next.expressconsent.com/v1/domains"
```

The capture itself is not part of this API; records are created by the browser SDK. See
[captureCDR()](https://app.expressconsent.com/docs/reference/capture-cdr).

## Base URL

```
https://api-next.expressconsent.com
```

Every path begins `/v1/`. Trailing slashes are ignored, and a path parameter is a single segment: a
record identifier containing a `/` will not match.

## Authentication

Every endpoint takes an API key in the `X-API-Key` header. The value is the key identifier and the
secret joined by a single dot, exactly as the dashboard gives it to you. A value with any other number
of dots is rejected as malformed.

```bash
X-API-Key: key_8f3c1e2a6b0d4f19.d41d8cd98f00b204e9800998ecf8427e
```

Create keys under **Organization → API keys**. The secret is shown once, at creation, and is stored
only as a hash. If you lose it, issue a new key and revoke the old one.

A key identifies an organization, and that is the only thing it identifies. Every request is scoped to
the organization that owns the key: you can read records captured on your own domains and records
shared into your account, and nothing else.

One route is exempt. `GET /v1/shares/:token` is the browser-openable form of a share URL and answers
`302` to the dashboard without reading a key and without a JSON envelope, so that a share URL pasted
into an email is a working link for a person. Receiving a shared record programmatically is a `POST` to
the same path, and that does take a key.

> **Warning: Never put an API key in browser code**
>
> A key grants full read access to every record your organization holds, and there is no way to scope
> one down. The API sends no CORS headers, so a browser request fails anyway, but the reason not to
> do it is the key rather than the CORS error. Call this API from your server.

Revoking a key, or having API access switched off, takes effect within five minutes rather than
immediately. Treat a leaked secret as live for that long.

## Response envelope

Every JSON response is wrapped, which is every response except the share redirect above. Read `ok` to
tell success from failure, and do not rely on the HTTP status alone, because one error is returned with
a 2xx status.

Success:

```json
{
  "ok": true,
  "data": { "domains": ["example.com"] },
  "requestId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301"
}
```

Failure:

```json
{
  "ok": false,
  "error": {
    "code": "CDR_INVALID",
    "message": "This CDR has been marked as invalid and cannot be shared",
    "requestId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301"
  }
}
```

**`ok`** (`boolean`, required)

`true` on success, `false` on failure. The only reliable success test.

**`data`** (`object`, required)

The endpoint's payload, on success only. Its shape is documented per endpoint.

**`error.code`** (`string`, required)

A stable machine-readable code, on failure only. Branch on this rather than on `message`. Every
code is on [API errors](https://app.expressconsent.com/docs/reference/api/errors).

**`error.message`** (`string`, required)

A human-readable explanation. Written for your logs, not for your users, and not stable. Do not
parse it.

**`error.details`** (`object`, optional)

Structured detail for the errors that have any. Two do: a rejected `acceptance` object carries
`issues[]`, and a record that failed [acceptance criteria](https://app.expressconsent.com/docs/reference/api/acceptance)
carries `failures[]` alongside an `outcome` of `rejected`.

**`requestId`** (`string`, required)

A unique identifier for the request. **Top-level on success, but nested as `error.requestId` on
failure**, so reading `body.requestId` gives you nothing on exactly the responses you care about. Log
it either way. It is the fastest way for us to find a specific request when you report a problem.

## Pagination

Only [listing the records on a domain](https://app.expressconsent.com/docs/reference/api/cdrs) paginates. Request a page size,
then follow the token.

**`pageSize`** (`integer`, optional, default `20`)

How many records to return.

Constraints: 1 to 100. A fractional value is truncated toward zero. A value outside the range after truncation, or one that is not a number, is replaced by the default rather than rejected.

**`pageToken`** (`string`, optional)

The `nextPageToken` from the previous response. A token that does not correspond to a record on
this domain is a `400 INVALID_ARGUMENT`.

**`order`** (`string`, optional, default `desc`)

Sort direction on the record's creation time.

Constraints: Only the exact string `asc` selects ascending order. Every other value, including a misspelling, means `desc`.

The response carries `nextPageToken` when more records exist and omits it on the last page. Stop when
it is absent rather than when a page comes back short. A page can contain fewer records than you
asked for and still not be the last one.

```bash
curl -sS \
  -H "X-API-Key: $EC_API_KEY" \
  "https://api-next.expressconsent.com/v1/domains/example.com/cdrs?pageSize=50&pageToken=$TOKEN"
```

## Requests

`POST` bodies are JSON, and every one is optional: each endpoint that takes a body has defined
behavior for an empty one. Bodies are strict where they are validated: an unrecognized field in
acceptance criteria is an error rather than being ignored.

Responses are never cached; every one carries `Cache-Control: no-store`.

An unknown path is a `404`, and a known path with the wrong method is a `405` carrying an `Allow`
header that lists the methods it does accept. Neither checks your API key first, so a `404` from a
mistyped path tells you nothing about whether your key is valid.

## Errors

Failures use the envelope above with a code from a fixed set. The full table, with the HTTP status for
each and whether retrying helps, is on [API errors](https://app.expressconsent.com/docs/reference/api/errors).

One status is worth knowing before you write a client: receiving a shared record that has not finished
rendering yet returns **`202` with `ok: false`**. A client that treats any 2xx status as success will
log a success and find no data.
