One candle
Open: 1.08432 High: 1.08456 Low: 1.08421 Close: 1.08443
You know the range. You do not know the path, the sequence, or what the spread was doing at the high.
GET /v1/ticks returns the stored bid/ask samples for an hour-sized window,
ascending, one row per second. Each row is stamped with the GMT second of the publish
batch it arrived in — so it is second-resolution quote history, not a reconstruction of
every individual market print. It is the finest resolution the platform stores.
{ "success": true, "data": { "symbol": "EURUSD", "ticks": [ { "time": "2026-09-18T14:32:00+00:00", "bid": 1.08432, "ask": 1.08445, "flags": 0 }, { "time": "2026-09-18T14:32:04+00:00", "bid": 1.08435, "ask": 1.08447, "flags": 0 }, { "time": "2026-09-18T14:32:08+00:00", "bid": 1.08421, "ask": 1.08436, "flags": 0 }, { "time": "2026-09-18T14:32:12+00:00", "bid": 1.08425, "ask": 1.08438, "flags": 0 }, { "time": "2026-09-18T14:32:16+00:00", "bid": 1.08456, "ask": 1.08469, "flags": 0 } … one object per sample, ascending, whole seconds, flags always 0 ], "count": 874 } }
An M1 candle tells you the extremes of a minute. It cannot tell you the order they happened in, how long price spent near either end, or how the spread behaved while it got there — and those are precisely the questions a second-by-second series answers.
Open: 1.08432 High: 1.08456 Low: 1.08421 Close: 1.08443
You know the range. You do not know the path, the sequence, or what the spread was doing at the high.
14:32:00 1.08432 / 1.08445 spread 0.00013 14:32:04 1.08435 / 1.08447 spread 0.00012 14:32:08 1.08421 / 1.08436 spread 0.00015 14:32:12 1.08425 / 1.08438 spread 0.00013 14:32:16 1.08456 / 1.08469 spread 0.00013 … 14:32:56 1.08443 / 1.08456 spread 0.00013
Both sides of the book on every sampled second — so the path, its order and the execution cost along it are recoverable. Spacing follows the update cycle of our data, so expect gaps rather than a row on every clock second.
Note what is not in the row. There is no spread field and no
volume field in the response. Spread is ask − bid, computed by
you; volume is not collected for samples, so anything that needs traded size — volume bars,
VWAP — needs /v1/ohlc instead. flags is in the payload but is
always 0.
There is no default window and no limit. That is deliberate: an
open-ended tick query is an expensive accident, so the endpoint makes you name the hour
you want.
| Parameter | Type | Required | Behaviour |
|---|---|---|---|
| symbol | string | required | Canonical symbol, e.g. EURUSD. |
| from | string | required | ISO 8601 start of the window. A trailing Z is accepted; a naive value is treated as UTC. Unparseable is a 400 with INVALID_DATETIME. |
| to | string | required | ISO 8601 end of the window. Must be at or after "from" — inverted is a 400 with INVALID_TIME_RANGE rather than an empty result set, because an empty 200 hides the caller's bug. |
Three of these four are validation and easy to handle. The one to design around is the row ceiling, because it is the only limit that does not raise — it truncates.
| Error code | Status | When |
|---|---|---|
| PLAN_UPGRADE_REQUIRED | 403 | The key’s plan is not one of pro or enterprise. The detail carries current_plan and an upgrade_url, so a client can route the user rather than guess. |
| INVALID_DATETIME | 400 | Either timestamp failed ISO 8601 parsing. The detail names the offending value. |
| INVALID_TIME_RANGE | 400 | "to" is before "from". Both values are echoed back. |
| RANGE_TOO_LARGE | 400 | The window exceeds 1 hour. The detail carries requested_range and max_range. |
Where the real ceiling is. The query carries a LIMIT of
50,000 rows, but it is also DISTINCT ON a whole-second timestamp — so
an hour cannot hold more than 3,600 rows and that LIMIT is never the
binding constraint on an hour-sized window. The ceiling that matters is the resolution
itself: one row per second, and only for the seconds a publish batch landed on.
Because the window is capped, any longer period is a loop over hour-sized slices — and
because the row cap is silent, that loop is also where you check count.
Derive the spread yourself; the tick row does not carry one.
Z is accepted. count against 50,000 on every response. historical permission scope. # from and to are BOTH required, and the window is capped at 1 hour curl -H "X-API-Key: YOUR_API_KEY" \ "https://tickatlas.com/v1/ticks?symbol=EURUSD&from=2026-09-18T14:00:00Z&to=2026-09-18T15:00:00Z"
# walk a longer period one hour at a time from datetime import datetime, timedelta, timezone URL = "https://tickatlas.com/v1/ticks" H = { "X-API-Key": KEY } start = datetime(2026, 9, 18, 12, tzinfo=timezone.utc) end = start + timedelta(hours=4) while start < end: stop = min(start + timedelta(hours=1), end) d = requests.get(URL, headers=H, params={ "symbol": "EURUSD", "from": start.isoformat(), "to": stop.isoformat(), }).json()["data"] # one sample per second at most, so count <= 3600 for a full hour print(start, d["count"]) # spread is DERIVED; there is no tick["spread"] for t in d["ticks"]: spread = t["ask"] - t["bid"] start = stop
// from and to are BOTH required and may not span more than one hour const qs = new URLSearchParams({ symbol: "EURUSD", from: "2026-09-18T14:00:00Z", to: "2026-09-18T15:00:00Z", }); const res = await fetch("https://tickatlas.com/v1/ticks?" + qs, { headers: { "X-API-Key": KEY }, }); const body = await res.json(); // a non-2xx uses the same envelope, with error instead of data if (!res.ok) throw new Error(body.error.code); // RANGE_TOO_LARGE, ... const { data } = body; // { symbol, ticks, count } // spread is DERIVED; a row is { time, bid, ask, flags } and flags is always 0 const spreads = data.ticks.map((t) => t.ask - t.bid); const mean = spreads.reduce((a, b) => a + b, 0) / spreads.length; console.log(data.symbol, data.count, mean, Math.min(...spreads), Math.max(...spreads));
Timestamps are whole seconds, so two rows could otherwise
arrive stamped with the same second, in no defined order.
DISTINCT ON the timestamp keeps one deterministic row per second, so the
array you receive is a single coherent series.
data.
The envelope is { "success": true, "data": { … } }.
Three keys at the top, four inside each row.
| Field | Type | What it carries |
|---|---|---|
| symbol | string | The canonical symbol. |
| ticks | object[] | The stored samples, ordered by time ascending. Empty array — not a 404 — when the window holds nothing. |
| ticks[].time | string | ISO 8601 at whole-second resolution. Every row in an update of our data is stamped with that update’s GMT second, so the fractional part is always zero and there is at most one row per second. |
| ticks[].bid | number | Bid at the moment the sample was taken. Not null — the column is required. |
| ticks[].ask | number | Ask at the moment the sample was taken. Spread is ask − bid; the endpoint does not pre-compute it. |
| ticks[].flags | number | Always 0, so do not branch on this value. |
| count | number | Length of the ticks array. One row per second means a full hour tops out at 3,600, well inside the 50,000-row query limit. |
Ticks are purged on the same cutoff as M1 candles — 24 hours by default, configurable per deployment. This is the one limit on the page that is deliberately not plan-gated: a Pro key and an Enterprise key see exactly the same depth, so history beyond the window is a collection problem, not an upgrade.
Anything inside the last 24 hours is one hour-sized request away. That covers same-day execution review, intraday microstructure work and rebuilding non-standard bars for the current session.
For a longer archive, run a scheduled job that pulls each hour once it has closed and writes it to your own store. One call per hour is 24 calls of quota per symbol per day for continuous coverage.
count on every sliceTick queries are the heaviest thing this API does, so the guardrails are explicit and they fail early — before the database is touched, where that is possible.
The plan allowlist is the first thing the route evaluates, so an unentitled key gets a 403 without the symbol or the timestamps being touched.
upgrade_urlA to before from would return zero rows and look like "no data". It is a 400 with both values echoed instead, so a caller bug surfaces at the call site.
INVALID_TIME_RANGEDISTINCT ON (tick_time) keeps the series coherent: whole-second stamps would otherwise collide, so one deterministic row wins each second.
Its own step in the weighting table, above the 2× premium endpoints and below the 5× ones. The key needs the historical scope, as /v1/ohlc does.
X-API-Key: tk_…Ticks are the floor. Everything else on the platform is an aggregate of them — weights are the real usage multipliers, so reach for the cheapest resolution that answers the question.
It is the only REST endpoint that turns a pay-as-you-go or Starter key away. Those keys reach the rest of the REST surface — candles, quotes, indicators and their history, the screener, the heatmap, spreads — but a tick request returns 403.
/v1/ticks/v1/ticksEvery one of these needs the individual updates rather than a summary of them, which is the whole test for whether this endpoint is the right one.
Range, renko and second-based bars need the samples inside the minute rather than a pre-aggregated one. An hour of samples is enough input to build any bar whose boundary is a price move or a span of seconds. Volume bars are not possible here — the response carries no volume.
ask − bid on every sample is how a spread widening becomes a sequence rather than a single average — you see when it opened up and how long it stayed there. It is the layer beneath /v1/spread.
A second-resolution path between two candle closes is a far richer feature than the close alone. The 24-hour retention means the collection job is yours: pull, store, accumulate.
Who can call it, what a row actually contains, where the ceilings are — including the one that truncates silently — and how long the data lives.
Pro and Enterprise only. The route checks the plan against an explicit allowlist of exactly {pro, enterprise} before it does anything else, and anything outside it is a 403 with PLAN_UPGRADE_REQUIRED, the current plan name and an upgrade URL in the detail. Pay-as-you-go and Starter reach the candle, quote, indicator, screener, heatmap and spread endpoints, but not raw ticks.
The window is capped at 1 hour — a wider one is a 400 with RANGE_TOO_LARGE, with your requested range echoed back. There is no limit parameter. Timestamps are whole seconds and the query is DISTINCT ON the timestamp, so a full hour can hold at most 3,600 rows; the query's own 50,000-row LIMIT sits far above that and is not something an hour-sized window reaches.
Four fields: time, bid, ask and flags. time is ISO 8601 at whole-second resolution — every row in an update of our data is stamped with that update’s GMT second, so the fractional part is always zero. bid and ask are never null because the columns are required. flags is always 0. There is no spread field — derive it as ask − bid.
No, and the page does not claim it. What is stored is a bid/ask sample taken each time our data updates for that symbol, stamped with that update’s GMT second. So you get second-resolution history of where both sides of the book sat, not a reconstruction of every individual print. For studying spread behaviour, execution cost and the path price took between two candle closes that is the resolution the question actually needs; for order-by-order microstructure it is not, and we would rather say so than sell you an hour of data that cannot answer it.
Nothing yet. It is part of the response shape and the ingest path stores 0 for every row, so every tick you receive has flags 0. It is documented because it is in the payload, not because it carries information — do not branch on it.
No. The query is DISTINCT ON the whole-second timestamp, so the array holds one deterministic row per second: a single coherent series.
Ticks are purged on the same schedule as M1 candles, which is 24 hours by default and configurable per deployment. Retention is NOT plan-gated: a Pro key and an Enterprise key see exactly the same depth. Anything longer than that window is yours to collect and store — pull each hour as it becomes available and keep it.
One call against your daily quota, whether the response holds five ticks or fifty thousand. Its 3× weight is its own step in the weighting table, between the 2× endpoints and the 5× ones, and the key must carry the historical permission scope — the same scope /v1/ohlc and /v1/indicator/history require.
Not for ticks. There is a live WebSocket at /ws/v1/quotes, available on Starter and above, but it pushes quote messages — bid, ask and symbol — rather than the stored tick rows this endpoint serves. For tick history the access pattern is REST over an hour-sized window.
At most 3,600 — one per second — and in practice fewer, because the sample cadence is the update cycle of our data for that symbol and a symbol only produces a row when our data updates it. TickAtlas does not publish a per-symbol figure it cannot hold for every instrument. The honest way to size it is to call one quiet hour and one active hour for the symbol you care about and compare count.
One hour-sized GET returns the second-by-second series a candle compresses
away — enough to measure spread behaviour and execution cost rather than infer them.
Available on Pro and Enterprise, weighted 3×, with 24 hours of depth on both.