Skip to content

API reference

Endpoints

POST https://ecapi.openap.tv/v1/events

A test endpoint mirrors production behavior but writes to an isolated test data set, for integration verification:

POST https://ecapi.openap.tv/v1/events/test

Your exact production and test base URLs are confirmed during onboarding.

All requests require authentication and TLS 1.3.

Request format

Requests use JSON:

Content-Type: application/json
Accept: application/json

Single event or batch

Send a batch with the events wrapper:

{
  "events": [ { /* Event Object */ }, { /* Event Object */ } ]
}

Or send a single event as a bare object (no wrapper):

{ /* Event Object */ }

Both forms are accepted. The Event Object itself is documented field-by-field in The Event Object.

Limits

Limit Value
Maximum request payload size 1 MB
Maximum events per request 1,000

Requests exceeding either limit are rejected with 400 Bad Request.

Responses

Status codes

Code Meaning When
200 OK All events in the request were accepted into the durable queue.
207 Multi-Status A batch was partially accepted; some events failed validation. Inspect the body.
400 Bad Request The payload could not be parsed, exceeded a limit, or no events passed validation.
401 Unauthorized Authentication failed, or the principal is not authorized for the data_set_id.
404 Not Found Endpoint or version path is incorrect.
429 Too Many Requests Rate limit exceeded. A Retry-After header is returned.
500 Internal Service Error OpenAP could not durably queue the event. Retry.

Note. A request whose URL path or HTTP method doesn't match a valid endpoint is rejected by the HTTP gateway in front of the eCAPI service with a 403 and a message that can look like an authentication failure — see Wrong URL or method.

Success body (200)

{ "accepted": 1 }

Multi-status body (207)

A 207 is returned when a batch contains a mix of accepted and rejected events. Treat it as a signal to inspect errors and retry only the rejected events after fixing them.

{
  "accepted": 998,
  "rejected": 2,
  "errors": [
    { "index": 14,  "event_id": "abc123", "error": "missing required field: data_set_id" },
    { "index": 873, "event_id": null,     "error": "schema validation failed: invalid event_type" }
  ]
}
  • index — zero-based position of the failed event in your request.
  • event_id — the event's id if it had one, otherwise null.
  • error — the reason the event was rejected (see Error reference).

Request-rejected body (400)

If the whole request fails to parse or no events pass validation, you receive a 400. Parse/limit failures return a single error:

{ "error": "payload exceeds 1 MB limit" }

If the request parsed but every event failed validation, the body uses the same multi-status shape as 207 with accepted: 0:

{
  "accepted": 0,
  "rejected": 2,
  "errors": [
    { "index": 0, "event_id": "abc123", "error": "missing required field: timestamp" },
    { "index": 1, "event_id": "def456", "error": "schema validation failed: invalid event_type" }
  ]
}

Error reference

Per-event errors

These appear in the errors[].error field of a 207 or a 400 multi-status body.

Error message Cause What to do
schema validation failed: <detail> The event failed JSON Schema validation (bad type, unknown enum value, malformed field). Fix the event to match The Event Object.
missing required field: <field> A required field (data_set_id, timestamp, event_type, or conditionally custom_event/currency_code) is absent. Add the field.
invalid field <field>: <reason> A field is present but invalid. Correct the field per the reason.
hashed field <field> is not a 64-char lowercase hex sha256 A hashed identifier is not a valid SHA256 hex digest. Normalize and hash per Hashing, consent, and privacy.
principal not authorized for data_set_id <id> Your credentials are not authorized for that data_set_id. Verify the data_set_id; contact OpenAP. (At the request level this returns 401.)

Request-level errors

These are returned as { "error": "<message>" } with a 4xx/5xx status.

Status Error message Cause
400 payload exceeds 1 MB limit Request body over 1 MB.
400 invalid JSON: <detail> Body is not valid JSON.
400 payload must be an object or {events:[...]} Body is not an Event Object or an events wrapper.
400 no events in payload The events array was empty.
400 batch exceeds 1000 events More than 1,000 events in one request.
401 <auth error> Authentication failed or principal not authorized.
500 ingest backend unavailable OpenAP could not durably queue the request. Retry.

Wrong URL or method (gateway errors)

A request whose URL path or HTTP method doesn't match a valid endpoint (for example POST /v1/event instead of /v1/events, or GET /v1/events) never reaches the eCAPI service — it is rejected by the HTTP gateway in front of it with a 403 and one of the messages below. Despite appearances, these are not authentication failures — your credentials were never evaluated.

Message Cause What to do
Invalid key=value pair (missing equal-sign) in Authorization header (hashed with SHA-256 and encoded with Base64): '...' The request URL path or method doesn't match a valid endpoint, and the request carried an Authorization header. The gateway attempted to parse the header in a different credential format and failed; the Base64 value is a hash of your header, not part of the error. Check the URL and method against Endpoints.
Missing Authentication Token Same cause, for a request carrying no Authorization header (HMAC signing uses the X-OpenAP-* headers, not Authorization). The message is about the URL/method, not your credentials. Check the URL and method against Endpoints.

If you see either message, fix the request URL/method first — only debug your credentials once the error changes to a 401 from Request-level errors.

Rate limits

Default limits per data_set_id:

Limit Default
Sustained requests/second 1,000
Burst requests/second (over 60s) 5,000
Sustained events/second (across batches) 100,000

When a limit is exceeded the receiver returns 429 Too Many Requests with a Retry-After header — honor it and back off. Higher limits are available on review; request them during onboarding.

Retries and reliability

  • A 200/207 means accepted events are durably queued — you do not need to retry accepted events.
  • Retry on 500 (or network/timeout errors) — the request was not durably accepted. Because of (data_set_id, id) merge semantics, re-sending the same events with the same id is safe and will not create duplicates in the merged view.
  • On 429, back off and honor Retry-After.
  • On 207, fix and retry only the events listed in errors; the others were accepted.
  • Do not retry plain 400/401 without changing the request — the payload or credentials need fixing first.

Use exponential backoff with jitter for transient (429/5xx) retries.

Examples

All examples sign the request body per Authentication: X-OpenAP-Signature = hex(HMAC-SHA256(shared_secret, body)).

Single event (bare object)

KEY_ID="your-key-id"
SHARED_SECRET="..."   # issued by OpenAP

BODY='{
  "data_set_id": "12345678AA",
  "id": "order-12ab123456c1234",
  "timestamp": 1746558464,
  "event_type": "purchase",
  "value": 22.00,
  "currency_code": "USD",
  "source": "website",
  "user_data": {
    "customer_identifier": "e606e38b0d8c19b24cf0ee3808183162ea7cd63ff7912dbb22b5e803286b4446",
    "email_address": ["b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514"],
    "phone_numbers": ["7c8d08bf90ca322bcac37f99ba14e980e4b1f5ba919f7e16a030af9070ba125e"],
    "address": [
      {
        "first_name": "81f8f6dde88365f3928796ec7aa53f72820b06db8664f5fe76a7eb13e24546a2",
        "last_name": "6627835f988e2c5e50533d491163072d3f4f41f5c8b04630150debb3722ca2dd",
        "city": "new york",
        "state": "ny",
        "country_code": "us",
        "postal_code": "e443169117a184f91186b401133b20be670c7c0896f9886075e5d9b81e9d076b",
        "address_type": "billing"
      }
    ],
    "gpp_string": "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
    "gpp_sid": [7]
  },
  "properties": {
    "transaction_id": "AAA123456789A",
    "page_url": "https://www.example.com/checkout",
    "items": [
      { "id": "SKU-123", "name": "Example product", "price": 22.00, "quantity": 1 }
    ]
  }
}'

SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SHARED_SECRET" | awk '{print $NF}')

curl -X POST https://ecapi.openap.tv/v1/events \
  -H "X-OpenAP-Key-Id: $KEY_ID" \
  -H "X-OpenAP-Signature: $SIGNATURE" \
  -H "X-OpenAP-Data-Set: 12345678AA" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d "$BODY"

Response:

{ "accepted": 1 }

Batch

BODY='{
  "events": [
    { "data_set_id": "12345678AA", "id": "e1", "timestamp": 1746558464, "event_type": "page_view", "source": "website" },
    { "data_set_id": "12345678AA", "id": "e2", "timestamp": 1746558470, "event_type": "add_to_cart", "source": "website" }
  ]
}'

SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SHARED_SECRET" | awk '{print $NF}')

curl -X POST https://ecapi.openap.tv/v1/events \
  -H "X-OpenAP-Key-Id: $KEY_ID" \
  -H "X-OpenAP-Signature: $SIGNATURE" \
  -H "X-OpenAP-Data-Set: 12345678AA" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d "$BODY"

Partial-success response:

{
  "accepted": 1,
  "rejected": 1,
  "errors": [
    { "index": 1, "event_id": "e2", "error": "schema validation failed: <detail>" }
  ]
}

Python

import hashlib, hmac, json, requests

url = "https://ecapi.openap.tv/v1/events"
key_id = "your-key-id"
shared_secret = b"..."  # issued by OpenAP

payload = {
    "data_set_id": "12345678AA",
    "id": "order-12ab123456c1234",
    "timestamp": 1746558464,
    "event_type": "purchase",
    "value": 22.00,
    "currency_code": "USD",
    "source": "website",
}

# Serialize once, sign those exact bytes, and send the same bytes.
body = json.dumps(payload).encode("utf-8")
signature = hmac.new(shared_secret, body, hashlib.sha256).hexdigest()

resp = requests.post(
    url,
    data=body,
    headers={
        "X-OpenAP-Key-Id": key_id,
        "X-OpenAP-Signature": signature,
        "X-OpenAP-Data-Set": "12345678AA",
        "Content-Type": "application/json",
        "Accept": "application/json",
    },
)
resp.raise_for_status()
print(resp.json())  # {"accepted": 1}