- Live wait times for every park
- Opening hours and the next 14 days of the crowd forecast
- Last 48 hours of ride history
- 60 requests a minute, 5,000 a day
- Commercial use with an attribution link
Public API · v1
Build with live
theme park data.
Live queue times, opening hours and crowd forecasts for 100+ parks, behind one set of JSON endpoints. Free to use - the only cost is an attribution link back to us.
Introduction
One read-only JSON API over everything this site runs on: which parks exist, what every ride's queue is right now, when each park is open, how busy we expect each day to be, and what those queues were in the past.
- Base URL
https://api.parkqueuetimes.com- Protocol
- HTTPS only. Every endpoint is a
GETand returns JSON. - Authentication
- An
x-api-keyheader on every request. Keys are free. - Versioning
- The surface is
/v1. Additive changes ship in place; a breaking one would ship as/v2. - Machine-readable
- OpenAPI 3.1 at
/v1/openapi.json- no key needed, so you can point a code generator at it right now. - Coverage
- 100+ parks and 4149+ rides across Europe, North America and Asia. Browse the list.
Quickstart
Three steps, about a minute, no card.
Create a free account
No card, no trial - register with an email address, or sign in with Google or Apple.
Create an API key
Head to API keys in your dashboard and create one. You can view and copy it from there any time.
Call the API
Send the key in the
x-api-keyheader and you are live:
curl -H "x-api-key: pqt_live_..." \
https://api.parkqueuetimes.com/v1/parks That is the whole integration. Every response carries a ready-made credit string in meta.attribution - show it near the data and you have met the one condition of the free tier. Next: how keys travel, or jump straight to the endpoint reference.
Authentication
One key per account, sent on every request.
Create your key at Developer in your dashboard with any account. You can view and copy it from the dashboard whenever you want, and revoke and recreate it as often as you need - there is no cooldown and nothing resets, because your rate limits, your plan and your usage history belong to the account rather than to the key.
Send it in the x-api-key header. The ?api_key= query parameter also works, which is handy for a quick test in a browser, but prefer the header in anything real: query strings end up in server logs, proxy logs and browser history.
curl -H "x-api-key: pqt_live_..." \
https://api.parkqueuetimes.com/v1/parks/3/liveA key identifies your account, not one of your users. Keep it on a server you control. If one does leak, revoke it and make another - it takes a second and costs you no history.
One favour, not a rule: send a User-Agent that names your project, like MyParkApp/1.0 (https://myapp.com). Because you should be calling us from your own server, that header is the only thing that tells us your requests apart from everyone else's default node or python-requests. It means we can warn you before something changes, and see that you are attributing us properly. Adding the URL where your key runs in the dashboard does the same job.
Response format
Every endpoint answers in the same envelope, so one parser and one error handler cover the whole API.
{
"success": true,
"data": { … },
"meta": {
"version": "1.0",
"generated": "2026-06-11T14:32:08.000Z",
"attribution": "Powered by ParkQueueTimes.com - https://parkqueuetimes.com",
"requestId": "req_8f2c1ad4"
}
}data is the only part that changes shape between endpoints. meta always carries these four:
version- The contract version this response was built against, currently
1.0. generated- When we produced the response, as an ISO 8601 timestamp in UTC.
attribution- The exact credit string to display. See Attribution.
requestId- Correlates this response with our logs. Quote it if you report a problem.
Some endpoints add one more - total on the park list, count and nextCursor on history, and access on history coverage. Additions are not breaking changes: new fields can appear inside data or meta at any time, so parse defensively and ignore what you do not recognise. Anything that would remove or repurpose a field would ship as /v2 instead.
Rate limits
Lenient by the minute, firm by the day - and every keyed response tells you exactly where you stand.
Your allowance depends on your plan - 60 requests a minute and 5,000 a day on the free tier, more on Developer and Archive. Limits are counted per account, not per key.
The per-minute limit is enforced leniently: a brief burst above your allowance will not cut you off, and only sustained traffic well above it earns a 429 Too Many Requests. Please do not design around that headroom - it is a courtesy rather than part of your allowance, and the published number is the one we enforce against.
The daily limit is firm. Once your account has used its allowance for the day, further requests return 429 with the code DAILY_LIMIT_EXCEEDED until it resets. Every 429 carries a Retry-After header in seconds - for a daily limit that is the time left in the day, so you can sleep on it rather than poll.
| Header | Meaning |
|---|---|
| X-RateLimit-Limit | Your per-minute allowance on the current plan. |
| X-RateLimit-Remaining | Requests left in the current minute. |
| X-RateLimit-Reset | Unix timestamp, in seconds, when the current minute window ends. |
| X-RateLimit-Daily-Limit | Your allowance for the day, counted across every key on the account. |
| X-RateLimit-Daily-Remaining | Requests left today. |
| X-RateLimit-Daily-Reset | Unix timestamp, in seconds, when the daily allowance resets. |
| Retry-After | Sent only with a 429. How many seconds to wait before trying again. |
Keyed responses are never cached (Cache-Control: no-store), so a live wait time is always the freshest reading we hold rather than something an edge served you.
Errors
Errors use one envelope too, with a machine-readable code and the request id to quote if you need to ask us about it.
{
"success": false,
"error": "A free key can read history from 2026-06-09 onwards.",
"code": "HISTORY_TIER_REQUIRED",
"requestId": "req_8f2c1ad4"
} The error string is written for a human and its wording may change. Branch on code:
| Status | Code | What it means, and what to do |
|---|---|---|
| 400 | BAD_REQUEST | A malformed parameter - a bad date, an unknown granularity, or a history range wider than one request allows. The error names which one. |
| 401 | UNAUTHORIZED | Missing, invalid or revoked key. Check the x-api-key header; keys are free if you need one. |
| 403 | HISTORY_TIER_REQUIRED | Your plan cannot read that far back. The error names the earliest date you can fetch - see Plans. |
| 404 | PARK_NOT_FOUND | Unknown park id or slug. List valid ids with /v1/parks. |
| 429 | RATE_LIMITED | Sustained traffic well above your per-minute limit. Back off for the number of seconds in Retry-After. |
| 429 | DAILY_LIMIT_EXCEEDED | Your account has used its allowance for the day. Retry-After gives the seconds until it resets; a larger allowance comes with Developer or Archive. |
| 503 | UNAVAILABLE | Park data is still loading or briefly unavailable. Retry after a short delay. |
A 5xx is ours, not yours - retry with a short backoff, and send us the requestId if it keeps happening.
Pagination
Only history pages. Every other endpoint returns its whole result in one response.
One history request covers at most 31 days hourly, 366 days daily or 1,830 days monthly — so a monthly series reaches back to January 2024 in a single call — and returns one page of rows. When meta.nextCursor comes back non-null there is more: repeat the identical request with &cursor= set to that value. When it is null, the window has been fully delivered.
let cursor = null
const rows = []
do {
const url = new URL('https://api.parkqueuetimes.com/v1/parks/3/history')
url.searchParams.set('from', '2026-06-01')
url.searchParams.set('to', '2026-07-01')
if (cursor) url.searchParams.set('cursor', cursor)
const res = await fetch(url, { headers: { 'x-api-key': process.env.PQT_KEY } })
const body = await res.json()
if (!body.success) throw new Error(body.error)
rows.push(...body.data.rows)
cursor = body.meta.nextCursor
} while (cursor) Cursors are opaque and short-lived - do not store one or try to build one yourself. To resume a backfill days later, remember the last date you wrote and start a fresh window from there.
Attribution
The one condition attached to a free key.
Free use requires displaying "Powered by ParkQueueTimes.com" linking to https://parkqueuetimes.com somewhere prominent in your app or service.
To make that easy, every successful response includes the exact string to show, in meta.attribution. Display it near the data and you are done. Apps and sites using our data without attribution may have their keys revoked. Paid plans drop the requirement.
Plans
Live data is free for everyone. Paid plans buy the full crowd forecast, history - how far back into the per-ride archive your key can read - higher limits and commercial use without attribution. All three license the data for your own product and your own analysis; publishing it or supplying it onward is Enterprise.
Developer
For apps and sites built on the data.
£15
No commitment, cancel any time
- Everything in Free and:
- The full crowd forecast, as far ahead as we predict
- Rolling 90 days of ride history
- 300 requests a minute, 50,000 a day
- Commercial use, no attribution needed
- Every key on your account upgraded at once
Archive
For crowd models, research and anything that needs the full series.
£40
- Everything in Developer, plus:
- Full history back to January 2024
- Hourly, daily or monthly, per ride or per park
- 600 requests a minute, 200,000 a day
- Enough to fit a model on, and to keep it fed
Checkout is tied to the account you are signed in with, and your existing key is upgraded automatically within a minute of paying - no new key, no code change. What you may and may not do with the data is set out in the API data licence - in short, build what you like on it, publish what you conclude from it, but do not re-supply the data itself. Anything beyond that is Enterprise.
Compare every detail
| Feature | Free | Developer | Archive | Enterprise |
|---|---|---|---|---|
| History | Last 48 hours | Rolling 90 days | Everything, from Jan 2024 | Everything, from Jan 2024 |
| Crowd forecast | Next 14 days | Full horizon | Full horizon | Full horizon |
| Grain | Hourly, daily or monthly | Hourly, daily or monthly | Hourly, daily or monthly | Any, including bespoke cuts |
| Requests per minute | 60 | 300 | 600 | Agreed |
| Requests per day | 5,000 | 50,000 | 200,000 | Agreed |
| Commercial use | With attribution | Included | Included | Included |
| Licence | Your own product and analysis | Your own product and analysis | Your own product and analysis | Publish and supply onward |
| Attribution link | Required | Optional | Optional | Optional |
| Billing | Free forever | Monthly or yearly | Yearly | Per project |
On Free, Developer and Archive, forecasts and recommendations derived from the data inside your own product are fine, and so is publishing what you conclude from it; re-supplying the data itself - as a feed, a dataset, or a deliverable someone else pays you for - is Enterprise.
API reference
Seven endpoints, all GET, all returning the same envelope.
- Base URL
https://api.parkqueuetimes.com{id}- The numeric park id from
/v1/parks. A parkslugis accepted as a convenience alias, but ids are stable and slugs are not - use the id in anything you store. - Samples
- Every request below reads your key from a
PQT_KEYenvironment variable, so you can export yours once and paste any of them straight into a terminal.
GET/v1/parks
Every park we track, with its location, timezone and links to its own sub-resources. Start here: the ids in this response address every other endpoint.
meta.total carries the number of parks returned.
Returns
idinteger- The canonical park id. Stable - store this one.
namestring- Display name.
slugstring- URL-safe alias. Accepted in place of an id, but it can change - never store it as a key.
countrystring- ISO country code.
latitude, longitudenumber- Park centre, in decimal degrees.
timezonestring- IANA zone. Opening hours and history buckets are in this zone.
linksobject- Absolute URLs for this park’s
self,live,scheduleandcalendarresources.
curl -H "x-api-key: $PQT_KEY" \
"https://api.parkqueuetimes.com/v1/parks"GET/v1/parks/{id}
One park, plus whether it is open right now and today’s hours.
Every field from the park list is present here too.
Path parameters
idinteger | stringrequired- Park id, or slug.
Returns
statusstringOPENorCLOSED, for the park as a whole.hoursobject | null- Today’s opening and closing times as ISO 8601 with the park’s own UTC offset.
nullon a day the park does not open, or when we hold no hours for it.
curl -H "x-api-key: $PQT_KEY" \
"https://api.parkqueuetimes.com/v1/parks/3"GET/v1/parks/{id}/live
Live standby wait times for every ride in the park, as of the park’s most recent publication to its own feed.
There is no fixed refresh cycle: readings change when a park publishes a change. Polling faster than about once a minute per park will not get you fresher data.
Path parameters
idinteger | stringrequired- Park id, or slug.
Returns
statusstring- One of
OPERATING,DOWN,CLOSED,REFURBISHMENT. waitMinutesinteger | null- Standby queue in minutes.
nullmeans the ride is publishing no number - which is not the same as a zero-minute walk-on. Never render a null as 0. lastUpdatedstring | null- When the park last changed this ride’s reading, ISO 8601 UTC.
curl -H "x-api-key: $PQT_KEY" \
"https://api.parkqueuetimes.com/v1/parks/3/live"GET/v1/parks/{id}/schedule
The park’s operating calendar - one entry per scheduled day, in the park’s own timezone.
Path parameters
idinteger | stringrequired- Park id, or slug.
Query parameters
datestring- Month to return, as
YYYY-MM. Past and future months both work. Defaults to the current month; a malformed value is a 400 rather than a silent fallback.
Returns
datestringYYYY-MM-DD, park-local.openingTime, closingTimestring- ISO 8601 with the park’s UTC offset. A closing time can fall after midnight.
typestring- Usually
OPERATING. Special windows, such as ticketed evening events, carry their own type.
curl -H "x-api-key: $PQT_KEY" \
"https://api.parkqueuetimes.com/v1/parks/3/schedule?date=2026-07"GET/v1/parks/{id}/calendarFree key: next 14 days
Predicted crowd levels per day, from our Almanac forecasting model.
For a date that has already happened the figure is measured rather than predicted, so the same endpoint serves both.
Crowd levels are comparable within one park across dates, not between parks - 70 at a quiet park is not the same queue as 70 at a busy one.
A free key reads the past and the next 14 days; dates beyond that are left out of days, and meta.access.until says where your horizon ends. A Developer or Archive key reads the whole horizon.
Path parameters
idinteger | stringrequired- Park id, or slug.
Query parameters
datestring- Month to return, as
YYYY-MM. Past and future months both work. Defaults to the current month; a malformed value is a 400 rather than a silent fallback.
Returns
crowdPercentinteger | null- 0-100, higher is busier, scored against this park’s own busiest day rather than against other parks.
nullwhere we have no forecast for that date.
curl -H "x-api-key: $PQT_KEY" \
"https://api.parkqueuetimes.com/v1/parks/3/calendar?date=2026-07"GET/v1/parks/{id}/history/coverageFree on every key
What history we hold for a park, ride by ride - and, in meta.access, how far back your own key may actually read it.
This endpoint is deliberately free on every tier: diff it against your own store to see exactly which windows you are missing before deciding whether a paid plan is worth it.
Path parameters
idinteger | stringrequired- Park id, or slug.
Returns
firstDate, lastDatestring- The park-local window we hold anything at all for.
rides[].days, rides[].hoursinteger- How many distinct days and hours we hold for that ride - so you can tell a fully covered ride from a patchy one before you pay for either.
meta.access.fromstring- The earliest date your key may fetch. On a free key this is two days ago and
meta.access.upgradeis also present.
curl -H "x-api-key: $PQT_KEY" \
"https://api.parkqueuetimes.com/v1/parks/3/history/coverage"GET/v1/parks/{id}/historyFree key: last 48h
Historical wait times, bucketed in the park’s own calendar time — by hour, day or month, per ride or for the park as a whole.
Read the counts, not just avgWait. A genuine walk-on is avgWait: 0 with waitReadings above zero; a closed ride is openReadings: 0; and a missing row means we collected nothing at all, which is not a zero wait. Most datasets collapse those three together, and that quietly biases any model fitted on them.
Requesting a from older than your plan allows is a 403 that names the earliest date you can fetch - it never silently truncates the window.
A park-level avgWait is weighted by observation, not by ride: every reading counts once, so a ride that runs all day carries more of the average than one that opens for an hour. That is the same figure however you bucket it, because we aggregate from a stored sum rather than averaging averages. /v1/parks/3/history?from=2024-01-01&to=2026-10-01&granularity=month&groupBy=park returns one row per month for the whole archive.
Path parameters
idinteger | stringrequired- Park id, or slug.
Query parameters
fromstringrequired- First day to include,
YYYY-MM-DD, park-local. tostringrequired- End of the window,
YYYY-MM-DD, exclusive. granularitystringhour(default),dayormonth. One request covers up to 31 days hourly, 366 daily, or 1,830 monthly.groupBystringride(default) for one series per ride, orparkto collapse every ride into a single series for the park.granularity=month&groupBy=parkis one average wait per park per month — the shape most research and benchmarking work wants.rideinteger- Restrict to a single ride id.
cursorstring- The
meta.nextCursorfrom the previous page. See Pagination. limitinteger- Maximum rows in this page. Lower it if you are memory-bound; you cannot raise it past our own cap.
Returns
date, monthstring- The bucket, park-local.
date(YYYY-MM-DD) at hourly and daily grain;month(YYYY-MM) at monthly. hourinteger- Hour of the day, 0-23, park-local. Present only when
granularity=hour; a daily or monthly row carriesopenHoursinstead, and a monthly row also carriesopenDays. ridesinteger- Only when
groupBy=park, in place ofrideIdandrideName: how many rides actually posted a wait in the bucket. Watch it — a month where it drops is a month our coverage thinned, not a quieter month. readingsinteger- Samples we collected in the bucket, whatever the ride was doing.
openReadingsinteger- Of those, how many had the ride open.
0means it was closed for the whole bucket. waitReadingsinteger- Of those, how many carried an actual number. Open with
waitReadings: 0means the ride published no wait. avgWait, peakWaitnumber | null- Mean and maximum standby wait across
waitReadings.nullwhen there were none.
curl -H "x-api-key: $PQT_KEY" \
"https://api.parkqueuetimes.com/v1/parks/3/history?from=2026-06-11&to=2026-06-12"Frequently asked questions
Is the API free?
Yes. Free keys include 60 requests per minute and 5,000 per day - plenty for most apps, bots and dashboards. The only requirement is the attribution link.
How fresh is the data?
Live wait times update as soon as each park publishes a change to its official feed - there is no fixed refresh cycle. Keyed /v1 responses are never cached, so you always see the latest reading we hold.
Can I use it commercially?
Yes - commercial use is welcome on the free tier, as long as the "Powered by ParkQueueTimes.com" attribution is displayed prominently in your app or service. Paid plans drop the requirement.
Is historical data available?
Yes - wait history back to January 2024, hourly, daily or monthly, per ride or for the park as a whole, in each park's own calendar time. Coverage is free on every key and shows exactly what we hold; the rows themselves are a paid plan, with the last 48 hours open on a free key so you can build against real data first.
Can I get one average wait per park per month?
In one request: granularity=month&groupBy=park on history. That is the shape most benchmarking, research and trend work needs, and it saves re-deriving it from ride-level rows - which is easy to get subtly wrong, because averaging per-ride averages is not the same number as weighting by observation. Coverage depth varies by park, so check coverage first: around 29 parks reach back into 2024 and the rest start later.
What do the paid plans cost?
Two, plus a licence for anything published. Developer is £19 a month (£190 a year): the full crowd forecast horizon, a rolling 90 days of ride history, 300 requests a minute and commercial use without attribution - the plan for an app or a site built on the data. Archive is £490 a year for the full series back to January 2024 at 600 a minute - for fitting a crowd model on the whole thing. Derived forecasts are fine on both; redistributing the data as a competing feed is not. Research, consultancy and published work are an Enterprise licence, from £1,500 per project. Live wait times, opening hours and the next 14 days of the forecast stay free.
What does a zero wait mean in the history?
Exactly what it says - but read the counts, not just avgWait. openReadings: 0 means the ride was closed. openReadings above zero with waitReadings: 0 means it was open but publishing no number. avgWait: 0 with waitReadings above zero is a genuine walk-on. A missing row means we collected no reading at all - not a zero wait.
Can I generate a client from the spec?
Yes. The full OpenAPI 3.1 description is at /v1/openapi.json and needs no key, so you can point openapi-generator, Swagger UI or your editor at it directly.
How many parks are covered?
Around 100 parks and 4149+ rides across the UK, Europe, North America and Asia, with more added regularly. Browse the full list.
Where do I report issues or request features?
Email [email protected] - we read everything, and developer feedback shapes what we build next.
Ready to build?
Your first key takes less than a minute. Questions, bug reports and feature requests all go to [email protected] - we read everything, and developer feedback shapes what we build next.