API reference¶
Endpoints¶
A test endpoint mirrors production behavior but writes to an isolated test data set, for integration verification:
Your exact production and test base URLs are confirmed during onboarding.
All requests require authentication and TLS 1.3.
Request format¶
Requests use JSON:
Single event or batch¶
Send a batch with the events wrapper:
Or send a single event as a bare object (no wrapper):
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
403and a message that can look like an authentication failure — see Wrong URL or method.
Success body (200)¶
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'sidif it had one, otherwisenull.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:
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/207means 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 sameidis safe and will not create duplicates in the merged view. - On
429, back off and honorRetry-After. - On
207, fix and retry only the events listed inerrors; the others were accepted. - Do not retry plain
400/401without 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:
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}