Sign CDP ingest requests without a client SDK
The event-ingest gateway at/cdp/v1/:ingest_id/* (track, identify, page, screen, group, alias, and batch) accepts HMAC-signed requests only. API keys, browser sessions, and bearer tokens are rejected on this surface by design — nothing except a valid signature from a tenant-scoped ingest secret gets through. This guide shows how to mint the secret, exactly which bytes to sign, and how to produce the three signing headers in Node, Python, and Go using nothing but the standard library.
Reach for raw HTTP when the web and Node SDKs do not fit — a Go service, a serverless function, an edge worker, or an embedded environment. If you are already on Node, the CdpAnalyticsClient in @devotel-orbit/node does this signing for you; everything below is what it does under the hood.
1. Mint the ingest secret and read the ingest id
The ingest secret identifies your workspace to the gateway. Two ways to mint one:- Dashboard. Open Integrations → CDP → Sources, create a source, and copy the secret shown once. The source’s ingest id sits next to it.
- API.
POST /api/v1/cdp/secretsagainst your workspace’s API key:
idis the ingest id — paste it into the request URL as/cdp/v1/{id}/track. It is public-safe: it routes your request to your workspace but cannot sign anything on its own.plaintextis the signing key. Only the 6-characterprefixis retrievable afterwards (GET /api/v1/cdp/secrets); a lost secret has to be replaced, not recovered.- The plaintext value goes into an environment variable or your secrets manager. Keep it server-side, always. A browser that can read the secret can sign events as anyone, so client-side JavaScript must never see it. Browser eventing uses the public-key Web SDK instead (see section 6).
2. The wire contract
Every request to/cdp/v1/:ingest_id/{track|identify|page|screen|group|alias|batch} carries three headers:
The canonical string the signature covers is exactly three segments joined with periods:
<raw body> is the exact UTF-8 bytes of the JSON body you send — no reformatting, no key reordering, no whitespace change between what you sign and what goes over the wire. Two production rules follow from this:
- Serialize the body once, sign those exact bytes, and send those exact bytes. Never let a framework re-serialize between the two steps.
- Sign the string concatenation above with the secret as the HMAC key, then lowercase-hex encode the digest.
3. A runnable signing loop in three runtimes
Each example below builds onetrack call end to end. The variable names match the table above so you can diff the three against each other.
Node (18+)
Python (3.9+)
timestamp + "." + nonce + "." + body — in the Node example the same join happens inside one string. Both end at the identical canonical bytes the gateway rebuilds, so keep whichever shape is cleaner in your runtime.
Go (1.20+)
4. The same loop for /identify, /batch, and /alias
Only the URL and the JSON body change — the signing loop is byte-identical across all seven endpoints. Wrapping the steps above into a helper (sign(secret, timestamp, nonce, body) → headers) is the entire integration.
/identify upserts the contact and folds traits into the profile. Send it once when the visitor first authenticates, and pass the anonymousId your client captured before login so the pre-auth events recorded under that id are backfilled onto the contact:
contact_id and contact_created to the standard received / event_id envelope.
/batch flushes up to 200 events in one POST, Segment-style. Items can mix track, identify, page, screen, group, and alias; a failing leg never fails the whole batch:
count and a per-leg results array — inspect results[i].ok to retry just the legs that failed.
/alias folds an anonymous id into a known user id, merging two contact profiles when both exist. Use the alias shown above — userId is the surviving id, previousId is the one being folded in. Typical sequence: your site assigns an anonymousId on first visit, the visitor later signs up, you fire identify plus alias with previousId set to that anonymous id, and the pre-signup event history attaches to the customer’s profile. The response adds merge_action (merged, relinked, or noop) and the two target_contact_id / source_contact_id fields so you can confirm the fold landed.
5. Expected responses and failure modes
A request that passes the signature gate returns HTTP 200 with the standard envelope — the data block varies slightly by endpoint:deduped: truecomes back when you re-sent amessageIdyou already used (Segment IDs are idempotency keys): the event was accepted but not stored a second time. If you do not setmessageId, the server derives one from the payload and a minute bucket so network-blip retries stay safe — pass your own if your retry loop spans longer than a minute.contact_idis null ontrack/page/screen/groupwhen neitheruserIdnoranonymousIdresolved to a known contact.
- Re-serialized body. The canonical string used the pre-encoding form; the gateway signs the wire bytes. Sign exactly what you
POST. - Clock skew past 5 minutes. Sync the sender to NTP and send Unix seconds, not milliseconds.
- Nonce reuse. Generate 16 random bytes per request — do not use a counter across processes.
- Missing
v1=prefix. The signature header isv1=<hex>, not a bare hex string. - Larger than 1 MB body. Split into
/batchlegs; the gateway returns 413 on oversized payloads rather than truncating.
SCHEMA_VALIDATION_FAILED— a workspace-authored event schema ran in strict mode and rejected the payload. The response body carrieserror.validation_errors, an array of{ path, message, expected, received }entries naming the offending property.TRACKING_PLAN_VIOLATION— a workspace-authored tracking plan in strict enforcement rejected the event. The body carrieserror.details.violationswithtype,path,expected, andactualper violation. Start tracked events in soft mode — violations log without rejects — and flip to strict only when your producers are clean.
6. When to use a hand-rolled signing loop
Use raw HTTP + this guide when the Node SDK does not reach your runtime, when you need exact control over flush cadence, or when auditing requires the signing happen in-process rather than inside a third-party dependency. Prefer the SDK surfaces when you have a choice:- Node services —
@devotel-orbit/node’sCdpAnalyticsClientwraps this exact loop plus queueing and flush-on-exit. - Edge / SSR surfaces with a trusted secret — the Web SDK
OrbitCdpAnalytics(and its queued variantOrbitCdpAnalyticsQueue) signs with WebCrypto where the secret can stay in the server’s environment. - Pure browser eventing — never a signed-ingest story. The ingest secret is server-only by design; browser clients go through the public-key surface that carries no signing secret at all.
See also
- CDP event model — the shape of the events these calls persist
- CDP tracking plan — declare the contract that rejects or logs drift in producer payloads
- CDP API reference — full parameter lists for every ingest endpoint
- Web SDK — the
OrbitCdpAnalyticsclient that performs this signing server-side for you