Authentication
Updated Jul 28, 2026 · 1d ago
Read this before you copy a header from one Epok guide into another. Epok has two planes, and they do not accept the same set of credential forms. One API key works on both — but a header shape that authenticates fine against one endpoint can return a bare 401 against another, with no hint that the scheme, not the key, was the problem.
Two asymmetries cause nearly every report: Authorization: Token <key> is accepted on the app plane and not on the ingest plane, and the ingest plane matches the scheme name case-sensitively while the app plane does not.
The plane is decided by the path, not the hostname. Both ingest.getepok.dev and app.getepok.devserve paths from both planes — each front door routes a fixed list of paths one way and everything else the other. So “I am posting to the ingest host” tells you nothing on its own. Find your path in the two lists below.
What each plane accepts
| Credential form | Ingest planelogs, traces, shippers | App planemetrics, replay, queries |
|---|---|---|
Authorization: Bearer <key> | acceptedexact case — Bearer, not bearer | accepted |
Authorization: Token <key> | not accepted | accepted |
Authorization: Basic <base64>key as the username; password ignored | accepted | accepted |
X-API-Key: <key> | accepted | accepted |
?api_key=<key> | drains and GELF only/api/v1/drain, /api/v1/drain/vercel, /api/v1/drain/heroku, /gelf, /api/v1/gelf — not the other ingest routes | last-resort fallbackfor sources that cannot set headers; use a header if you can |
Capitalisation of the scheme is not symmetric. The app plane compares scheme names case-insensitively, so bearer, Bearer and BEARER all work. The ingest plane matches the literal prefix Bearer or Basic — a lowercase authorization: bearer <key> is legal HTTP and is nonetheless not parsed there, producing the same bare 401 as sending nothing. If you hand-roll the header, copy the capitalisation exactly. (Header names are always case-insensitive; this is only about the scheme word in the value.)
On both planes a header credential is resolved before any ?api_key= — the query parameter is a fallback, never an override. The two planes disagree on which header they read first, so do not send two different keys and expect a predictable winner.
The failure this table exists to explain
You wire up Telegraf against the line-protocol endpoint. Telegraf's outputs.influxdb_v2 emits Authorization: Token <key> and offers no way to send anything else, so the app plane accepts that scheme deliberately. Metrics flow. You now have a working, proven header shape and a key you have personally watched succeed.
A week later you point a log shipper at the ingest plane and reuse that same header. You get 401. Nothing about the response says “wrong scheme” — and because the key demonstrably works, the natural conclusion is that the key was revoked, the account is wrong, or the endpoint is down. None of those are true. The ingest plane simply never parses Token, so it sees a request carrying no credential at all.
# Works — /write is an app-plane path, and the app plane parses Token.
curl -i https://ingest.getepok.dev/write \
-H "Authorization: Token $EPOK_API_KEY" \
--data-binary 'probe,host=laptop value=1'
# 401 — same key, same HOST, different path. /api/v1/ingest is an
# ingest-plane path, and the ingest plane never parses "Token".
curl -i https://ingest.getepok.dev/api/v1/ingest \
-H "Authorization: Token $EPOK_API_KEY" \
-H "Content-Type: application/json" \
-d '[{"_msg":"auth probe"}]'
# Fix: Bearer or X-API-Key.
curl -i https://ingest.getepok.dev/api/v1/ingest \
-H "Authorization: Bearer $EPOK_API_KEY" \
-H "Content-Type: application/json" \
-d '[{"_msg":"auth probe"}]'Which plane is which
Same key, same dashboard, two different services behind the door. Both hostnames accept both lists: ingest.getepok.dev hands the metrics and replay paths to the app plane, and app.getepok.dev hands the log and trace paths to the ingest plane, so existing configs keep working either way. Match your path against these two lists — that, not the hostname, tells you which credential forms apply.
Ingest plane
Logs and traces, plus the shipper-compatible surfaces: /api/v1/ingest, /v1/logs, /v1/traces, Elasticsearch bulk (/_bulk, /insert/elasticsearch/_bulk), Loki push (/loki/api/v1/push), syslog (/api/v1/syslog), Fluent Bit / Fluentd (/api/v1/fluent, /api/v1/fluentd), CloudWatch (/api/v1/cloudwatch), Splunk HEC (/services/collector/event, /services/collector/raw), GELF, and the PaaS drains.
App plane
Metrics and everything you read back: OTLP metrics (/v1/metrics, /api/v1/otel/v1/metrics), Prometheus remote write (/api/v1/write), InfluxDB line protocol (/write, /api/v2/write), session replay (/v1/rum/replay), and every query endpoint under /api/v1/tenants/….
One asymmetry in that cross-routing. The ingest host forwards only the metrics and replay paths named above to the app plane — the six of them, by exact path. Query endpoints under /api/v1/tenants/… are served on app.getepok.dev only. Ask the ingest host for one and you get a 404, not an auth error, because the request never reaches the service that implements it. If a read call is 404ing with a key you know is good, check the hostname before you touch the key.
# Ingest plane — logs, traces, and the shipper-compatible endpoints.
curl -i https://ingest.getepok.dev/api/v1/ingest \
-H "X-API-Key: $EPOK_API_KEY" \
-H "Content-Type: application/json" \
-d '[{"_msg":"auth probe","service":"probe"}]'
# 200 {"status":"ok","accepted":1} -> the key works on this plane
# 401 -> no credential was parsed at all
# 403 -> a credential was parsed and rejected# App plane — metrics, line protocol, replay.
# Same hostname as the probe above. Only the PATH changed.
curl -i https://ingest.getepok.dev/write \
-H "Authorization: Bearer $EPOK_API_KEY" \
--data-binary 'probe,host=laptop value=1'
# 204 (no content) -> accepted
# 401 / 403 -> same meaning as aboveIf you are coming from Splunk
HEC clients send Authorization: Splunk <token>. Epok serves the HEC endpoints, but it does not parse the Splunk scheme — only Bearer, Basic, and X-API-Key. A HEC client left on its default header will 401 against /services/collector/event forever, no matter how correct the key is. Change the scheme:
# Splunk default — NOT parsed, always 401:
# -H "Authorization: Splunk $EPOK_API_KEY"
curl -i https://ingest.getepok.dev/services/collector/event \
-H "X-API-Key: $EPOK_API_KEY" \
-d '{"event":"hello from HEC"}'Triaging a 401
On API-key traffic a 401 from Epok means exactly one thing: no credential could be extracted from the request. It is not a statement about whether your key is valid — an unparsed header and an absent header produce the identical response. Walk these in order.
(Browser sessions are the one exception, and they are not what you are debugging here: a signed-in session whose token has aged out returns 401 Token expired from app.getepok.dev, which is a credential that was read and then refused. It applies to the UI, never to a shipper.)
- Is the scheme accepted on this plane? Check the header you are actually sending against the table above. This is the one that catches people, because the header is not malformed and works somewhere else.
Tokenagainst the ingest plane andSplunkagainst anything are the two common cases. - Is the key ingest-scoped? Ingest endpoints require a key carrying the
ingestscope. A read-only key is parsed successfully and then refused — which shows up as403, not401, so if you are seeing 401 this is not your problem. Check the scope in the app under Settings → API Keys. - Is the whole key present and clean? Surrounding whitespace is trimmed for you, but shell quoting is not something we can fix: an unset variable expands to an empty string and sends an empty credential, which reads as no credential. A truncated paste is the same story. Echo the header your client actually sends and confirm the key is complete.
- Is a stale header shadowing a good key? Header auth is resolved before
?api_key=on both planes. If your drain URL carries a correctapi_keyquery parameter but your client also sets an oldX-API-Key, the old header wins and the good query parameter is never read. Remove the header, or fix it — do not add the query parameter and hope it takes precedence. Like step 2, this lands as403, not401: the stale header parsed, so a credential was found. Worth knowing here because the symptom — “my URL has the right key and it still fails” — reads like an auth problem rather than a precedence one. Note the inverse is not a trap: a header the plane cannot parse (aTokenscheme on the ingest plane) does not shadow anything, and the query parameter is still used.
# The header is wrong, the query param is right. This returns 403,
# NOT 401 — the stale header parsed fine, so a credential WAS extracted,
# and the correct query param was never consulted.
curl -i "https://ingest.getepok.dev/api/v1/drain?api_key=$GOOD_KEY" \
-H "X-API-Key: old-rotated-key" \
-d '[{"_msg":"hello"}]'
# 403 {"error":"forbidden","detail":"Invalid API key. ..."}
# Drop the stale header and the query param is used:
curl -i "https://ingest.getepok.dev/api/v1/drain?api_key=$GOOD_KEY" \
-d '[{"_msg":"hello"}]'401 is not 403
The distinction is worth internalising, because it splits your search space in half:
- 401 — nothing usable was found in the request. Look at the shape of what you are sending: the scheme, the header name, whether the variable expanded.
- 403 — a credential was extracted and then rejected. The request is authenticated in form but not entitled. Causes: the key is unknown or has been rotated, the key has expired, the tenant is disabled, the key lacks the required scope (
ingestfor ingest endpoints), or the key does not match the tenant named in the URL path.
So a 403 is actually good news during setup — it proves your header shape is right and the plane read your credential. Plan and feature gates are covered in limits.
Where keys should live
Prefer a header wherever the source can set one. Keys in query strings end up in access logs, proxy logs, and browser history — ?api_key= exists for sources that genuinely cannot send headers (some PaaS log drains, some GELF shippers), not as a convenience. When you do use it, use a separate ingest-scoped key you can revoke on its own.
Keep keys in environment variables read at runtime rather than in committed configuration files. Every snippet on this page reads its key from the environment for that reason.
Next
- Quickstart — get a key and send your first data.
- Limits — what a 403 means in terms of plan and feature gates.
- Traces (APM) — the ingest plane in practice, including the protocol traps.