epok

Quickstart

Updated Jul 28, 2026 · 1d ago

One event in, one query out. This page is the shortest complete path — send a single log line with curl, confirm it arrived, and search for it. No agent, no collector, no config file. If you are wiring a real shipper or an SDK, start here anyway to prove the key and the network path work, then move to the install guides.

Time to first query: ~2 min · Trial: 14 days, no card · API key: app.getepok.dev → Settings → API Keys

Read this first: the timestamp trap

This is the one failure that looks like nothing went wrong. A _time in the past is honored as-is, at any age. An event dated last Tuesday is therefore stored last Tuesday — it is in Epok, searchable, correct — but it will not appear in Live Tail or in a recent search window, because that is not when it happened. Ingest still returns accepted, because ingest did its job.

Omit _time entirely and Epok stamps arrival time for you. That is what the snippet below does, and it is why the snippet is reliable. When a shipper backfills, or a log file is replayed from disk, you get the same symptom: data present, data invisible.

The two directions are not symmetric, which matters if you suspect a clock. Backwards is honored without limit; forwards is not. A _time more than 5 minutes ahead of the server is discarded and replaced with arrival time, and so is a _time Epok cannot parse. Both are silent. So a container whose clock runs fast produces the opposite symptom — events you expected in the future are sitting in the present, stamped on arrival, and no amount of searching forward will find them.

An ingest response saying accepted means the data was stored. If you cannot see it, the usual culprit is the timestamp, not the pipe. Search the period the data claims to be from before you suspect the network.

1. Send one event

Replace YOUR_API_KEY with a key from Settings → API Keys.

terminal
bash
curl -X POST https://ingest.getepok.dev/api/v1/ingest \
  -H 'X-API-Key: YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "_msg": "checkout service started",
  "level": "info",
  "service": "checkout"
}'

A success looks like this — accepted is the number of entries stored, so a 0 here means the body parsed but every entry was dropped or filtered:

json
{"status":"ok","accepted":1}

The body may be a single JSON object, an array of them, or NDJSON — all three are accepted at the same path. _msg is the message field; message and msg are accepted as aliases. Any other keys you send are kept as structured fields and become searchable. X-API-Key is one of several accepted header forms — see Authentication for the full per-plane scheme table, including the forms your shipper may hardcode.

2. Verify it landed

Open Live Tail in the app, or Explore with the range set to the last 15 minutes. This should be fast — the ingest path flushes continuously, not on a batch timer, so give it a few seconds, not minutes. The one case that takes marginally longer is the very first event from a brand-new key, which waits for the writer to notice a tenant it has never seen; call it ten seconds before you start debugging.

This matters for what you conclude next: if the event is still missing after that, waiting longer will not help. It is one of the two branches below.

If it did not work

Branch A — curl returned an error, nothing was accepted

The status code tells you which of three things went wrong, and they have nothing to do with each other:

  • 401 — the key is wrong, or the header form is. Not every scheme is accepted on every plane; check Authentication before you regenerate a key that was fine.
  • 413 — the body exceeded the ingest cap, which defaults to 2 MB per request (compressed bodies are measured after decompression). A single request is also capped at 50,000 entries. Split the batch and retry.
  • 429 — back off and retry; the response carries a Retry-After. Do not assume this means you outgrew your plan. The error field says which: rate_limitedis your plan's request rate or daily volume, but backpressure and wal_full are ours — the ingest path is briefly behind, and the same request will succeed on retry. See Limits.

Branch B — ingest returned accepted, but you see nothing

This is a different problem, and it is almost never the network. The data is stored. Two causes, in order of likelihood:

  • The timestamp — you sent a _time in the past, so the event landed in the past. Point the search at that period rather than just widening around it: a range over 6 hours is answered from the 3 newest hours that contain data, so on a service that is still logging, a 7-day search returns today and quietly skips last Tuesday. A narrow window over the hour you actually backfilled is scanned in full and will find it. When the range was narrowed, the response says so in adaptive_window. Then fix the shipper, or drop _time and let arrival time be stamped for you.
  • The window is too narrow — a 5-minute range leaves no room for a clock that is off by a minute, or for the gap between when you sent the event and when you looked. Search the last hour; it costs nothing here and removes the variable.

If a wide window still shows nothing and ingest said accepted, check that you are querying the same tenant the key belongs to — a key scoped to one tenant cannot read another's data, and that mismatch returns 403 rather than an empty result.

3. Your first query

Getting data in is not the finish line — reading it back is. This call needs your tenant id, which is not printed on the API Keys screen. The quickest way to read it: while signed in to the app, open app.getepok.dev/auth/me — the JSON it returns includes tenant_id.

terminal
bash
curl -X POST https://app.getepok.dev/api/v1/tenants/YOUR_TENANT_ID/search \
  -H 'X-API-Key: YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "query": "checkout",
  "start": "-1h",
  "end": "now",
  "limit": 100
}'

query is required. start and end accept relative offsets like -1h and now as well as absolute timestamps, and default to -1h and now. limit defaults to 1000 and caps at 10000. A before field takes a timestamp cursor to page backward through older results.

A successful response returns the matching entries in logs, how many came back in count, and how long the whole request took server-side in elapsed_ms. The response carries more fields than shown here — notably query_ms, the backend query time on its own, and has_more, which tells you there is another page:

json
{
  "logs": [
    {
      "_time": "2026-07-20T14:03:11.482Z",
      "_msg": "checkout service started",
      "level": "info",
      "service": "checkout"
    }
  ],
  "count": 1,
  "elapsed_ms": 34
}

The same query in the product: open Explore, type checkout in the query bar, set the range to the last hour. Bare words are a full-text match; you narrow from there with field filters like level:error and combine them with AND / OR. The full language — field filters, ranges, aggregation pipes — is in Search syntax.

What you do not need

For plain HTTP ingest, everything above is the whole dependency list: an API key and something that can make a POST request.

  • No collector. You are talking to the ingest endpoint directly.
  • No agent or sidecar to install, version, or keep alive.
  • No schema, index, or field mapping to declare up front. Send whatever fields you have; they become searchable as they arrive, and adding a new one later needs no migration.
  • No alert rules or thresholds to author before detection does anything. That is the next section.

What happens next

Detection switches on by itself once data flows. It is not equally good on day one and day seven, and it is worth knowing which is which so an early quiet week does not read as a broken install.

Immediately

Search and live tail

The moment your first event lands you can search it and stream it live. There is no indexing delay to wait out.

First cycle, then continuously

New-failure fingerprinting

Epok fingerprints failures and flags ones it has never seen before — a novel error message, a new failing operation. It needs no seasonal baseline, so it is useful within the hour rather than on day seven. One thing to expect: on its first pass over a new tenant it absorbs the error patterns already flowing as known, so the errors you are shipping at connect time do not page you. What alerts is what appears after that. Detection runs every 5 minutes.

~1 hour

Absence detection

Epok learns each service's expected cadence and starts flagging absence — a service that stops logging, a series that goes silent. These are the failures that emit nothing at all, so nothing else catches them.

~1 hour → day 7

Baselines mature

Detection starts within about an hour of first data, but with deliberately wider thresholds — it does not yet know your normal, so it errs toward staying quiet. The log-volume baseline matures in about three days, and thresholds keep tightening to full precision by day seven. During learning, pattern anomalies alert on errors first.

Send more than one signal and the detectors stop working in isolation: logs, metrics, and traces from the same service get correlated into a single incident rather than three separate pages. The signals do not all start at the same moment, though. Log detection is live as soon as logs arrive. Trace, metric, and RUM detectors compare a recent slice against a recent baseline with no stored history, so on a newly connected signal they would be scoring startup noise — they stay quiet for the first few hours after Epok first sees that signal, then switch on by themselves. A quiet trace page an hour after you wire OpenTelemetry is that gate, not a broken exporter.

Next steps

  • Search syntax — field filters, ranges, and aggregation pipes.
  • Install guides — replace the curl above with your real shipper, SDK, or cluster.
  • Detectors — what each one watches for and what its alerts look like.
  • Limits — body caps, rate limits, daily volume, and retention by plan.
  • Authentication — every accepted key form, per plane.