Run the Timelines query over many locations and deliver the result as a file to your own bucket, once or on a schedule.
Bulk Timelines runs the Timelines query for up to 50,000 locations in one request, and delivers the result as a file to your own bucket. Use it instead of many separate Timelines calls when you need weather data for a large set of locations. You can run an order once (on demand) or on a cron schedule, for example a new 5-day forecast for all your locations every 6 hours.
How it works:
- Set up your bucket. Contact Tomorrow.io for setting credentials. Refer to Destination.
- Create an order with your locations (or a assets feed), fields, time window, format and destination.
- Track the run until it is complete.
- Get the files from your bucket.
Orders and runsAn order is the stored configuration: the locations, fields, time window, format, destination and an optional schedule. Its ID is
orderId. You can pause, resume or rename an order.A run is one execution of an order. Its ID is
runId. A run cannot be changed. An on-demand order starts its run immediately. A scheduled order starts a run at each tick of its schedule.
Create an order
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
locations | array | One of locations or feedId | The locations, with a maximum of 50,000. Each item is a [lat, lon] pair, a "lat, lon" string, a GeoJSON geometry, or the ID of a saved location (a 24-character hexadecimal ID). Refer to formats. |
feedId | string | One of locations or feedId | The ID of a stored assets feed. The feed is resolved when each run starts. A feed that does not exist or is empty returns 422 feed_not_found. |
fields | array of strings | Yes | The fields to export from our available data layers. The maximum is 100 fields. |
format | string | Yes | The format of the delivered file: csv, json or jsonl. Refer to Data file. Any other value returns 400 invalid_format. |
destination | object | Yes | The bucket that receives the file. Refer to Destination. |
timesteps | array of strings | No | One or more timesteps. Refer to Timestep Availability. |
start | string | No | The start of the time window: an ISO 8601 time, now, nowPlusXm/h/d or nowMinusXm/h/d (for example nowPlus6h). |
end | string | No | The end of the time window. It accepts the same formats as start. If end is before start, the request returns 400 invalid_field. |
timezone | string | No | An IANA timezone name, for example America/New_York. Defaults is UTC.The timezone parameter displays the response datetime in the requested timezone. The timezone parameter is not affecting the request startTime and endTime. Using the auto value will automatically set the timezone of the request location. |
dailyStartHour | integer | No | This setting is used only with daily timesteps (1d) and determines when the daily aggregation period begins. By default, the aggregation runs from 6 AM to 6 AM. You can adjust this by setting dailyStartHour to any value between 0 and 23 (where 0 represents midnight, 1 represents 1 AM, and so on). For example, to aggregate from midnight to 11:59 PM, set dailyStartHour to 0. |
units | string | No | metric or imperial. Default is metric. Refer to Units for additional explanation. Any other value returns 400 invalid_units. |
name | string | No | A label for the order, with a maximum of 128 characters. It must be unique in your account. If not, the request returns 409 name_conflict. |
active | boolean | No | The default is true. If false, the order is stored but does not run until you activate it. Refer to Pause, resume or rename an order. |
trigger | object | No | Omit it for an on-demand order (one time request). For a scheduled order, refer to Scheduled orders. |
Request Example
curl -X POST "https://api.tomorrow.io/v4/data/orders/timelines?apikey=$TOMORROW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "warehouses-next-3-days",
"locations": [
[32.0853, 34.7818],
"40.7128, -74.0060",
{ "type": "Polygon", "coordinates": [[[-0.2,51.4],[0.1,51.4],[0.1,51.6],[-0.2,51.6],[-0.2,51.4]]] }
],
"fields": ["temperature", "windSpeed", "precipitationIntensity"],
"timesteps": ["1h"],
"start": "now",
"end": "nowPlus3d",
"units": "metric",
"format": "csv",
"destination": { "type": "gcs", "bucket": "my-weather-exports", "prefix": "tomorrow" }
}'Response 201 Created for an on-demand order:
{
"orderId": "ord-<accountId>-1a2b3c4d",
"name": "warehouses-next-3-days",
"runId": "3f1c2b9e-7a4d-4e8b-9c21-5d6e7f8a9b0c",
"dataset": "timelines",
"size": null
}size is always null for timelines orders. name is present only when you set one.
When you create an order, the API checks your query with the Timelines API. An unknown field name or a timezone that is not valid returns 400 invalid_field, and field names the problem. Thus you find query errors before a run starts.
Destination
Before you startSet up delivery to your bucket before you create an order. Contact Tomorrow.io with the bucket name, provider (GCS, S3 or Azure) and credentials. This is done once for each bucket. If you send an order to a bucket that is not set up, the request returns 422 missing_credential.
{ "type": "gcs", "bucket": "my-bucket", "prefix": "optional/prefix" }| Parameter | Description |
|---|---|
type | gcs, s3 or azure. Any other value returns 400 invalid_destination_type. |
bucket | The bucket name only. A value with gs://, /, : or spaces returns 400 invalid_field. |
prefix | Optional. A folder path for the files in the bucket. |
Scheduled orders
To make a recurring export, add a trigger with a 5-field cron expression in UTC. The request does not start a run. The response has nextRunTime and no runId.
Request for example:
{
"feedId": "my-sites-feed",
"fields": ["temperature", "humidity"],
"timesteps": ["1h"],
"start": "now",
"end": "nowPlus2d",
"format": "jsonl",
"destination": { "type": "s3", "bucket": "my-weather-exports" },
"trigger": { "type": "schedule", "schedule": "0 */6 * * *" }
}Response:
{
"orderId": "ord-<accountId>-5e6f7a8b",
"dataset": "timelines",
"nextRunTime": "2026-10-12T18:00:00Z",
"size": null
}- The minimum interval between runs is 5 minutes.
- A 6-field expression (with seconds), an expression that is not valid, or a schedule that runs more frequently than every 5 minutes returns
422 invalid_schedulewith"limit": "5m0s".
Relative time windowsOn a scheduled order, relative values such as
nowPlus2dis resolved at each run. In the example above, each run exports a new 2-day forecast. If you use ISO 8601 times, each run exports the same window.
Delivered files
Each run writes its files to a folder for that run. A new run never overwrites the files of an earlier run.
<bucket>/<prefix>/<accountId>/<orderId>/<runId>/timelines.<csv|json|jsonl>
<bucket>/<prefix>/<accountId>/<orderId>/<runId>/failures.json
<bucket>/<prefix>/<accountId>/<orderId>/<runId>/credits.json
<bucket>/<prefix>/<accountId>/<orderId>/<runId>/error.json
If you do not set a prefix, the path starts with <bucket>/<accountId>/.
| File | When it is written | Contents |
|---|---|---|
timelines.<format> | When at least one location succeeded | The weather data. Refer to Data file. |
failures.json | When at least one location failed | The locations that are not in the data file, and the reason for each. If all locations failed, this file is delivered without a data file. |
credits.json | When at least one location succeeded | How many credits were charged for the run. Refer to credits.json. |
error.json | When the whole run was refused, or when all locations failed with the same query error | order_id, run_id, code, message and time (UTC). |
Data file
CSV has one row for each location, timestep and interval. The columns are id, location, timestep and startTime, followed by one column for each field in the order that you requested them.
id,location,timestep,startTime,temperature,windSpeed
,"[32.0853,34.7818]",1h,2026-01-01T00:00:00Z,23.21,6.74
,"[32.0853,34.7818]",1h,2026-01-01T01:00:00Z,23.46,7.07
locationis the location as you sent it: the[lat,lon]pair or"lat, lon"string, or the GeoJSON as compact JSON.idis the ID of a saved location (thenlocationis empty), or the feed asset's key for a feed. For inline locations,idis empty.- A field with no value is an empty cell.
JSON is one array with one object for each location. JSONL has the same objects, one on each line. Each object has id and location (as in CSV, each only when it has a value) and timelines, which is the same as data.timelines in a Timelines API response:
{
"location": [32.0853, 34.7818],
"timelines": [
{
"timestep": "1h",
"startTime": "2026-01-01T00:00:00Z",
"endTime": "2026-01-06T00:00:00Z",
"intervals": [
{ "startTime": "2026-01-01T00:00:00Z", "values": { "temperature": 23.21, "windSpeed": 6.74 } }
]
}
]
}failures.json
{
"order_id": "ord-<accountId>-1a2b3c4d",
"run_id": "3f1c2b9e-7a4d-4e8b-9c21-5d6e7f8a9b0c",
"locations_total": 1000,
"locations_failed": 1,
"failures": [
{ "location": "40.7128, -74.0060", "reason": "rate_limited", "status": 429, "message": "..." }
]
}Each failure has reason and message. location_id is present for a saved location or a feed asset, and location is present for an inline location.
reason | Meaning |
|---|---|
location_not_found | The location was not found. |
invalid_request | The Timelines API refused the request for this location. |
not_entitled | Your plan does not include the data that was requested. |
rate_limited | The request was rate limited. |
insufficient_credits | Your account did not have enough credits. |
upstream_unavailable | A temporary error on our side. |
unknown | Any other error, for example no data for the location. |
credits.json
| Key | Description |
|---|---|
order_id, run_id | The order and the run. |
credits_used | The credits that the run used. Failed locations are not charged. |
billed_calls | The number of billed calls. |
complete | false if part of the run did not report its credits. Then credits_used can be too low. |
balance_remaining | Your remaining Timelines credit balance when the file was written. |
locations_total, locations_ok, locations_failed | The number of locations in total, that succeeded, and that failed. |
locations_insufficient_credits | The number of locations that failed because your account did not have enough credits. It is present only when this number is more than 0. |
error.json
code | Meaning |
|---|---|
permission_denied | Your identity was refused. |
feed_not_found | The feed does not exist, is empty, or you do not have access to it. |
too_many_locations | The feed has more than 50,000 locations. |
invalid_order | The order is not valid. |
invalid_query | The Timelines API refused the query for every location with the same error, for example an unknown field or a timezone that is not valid. Fix the order and run it again. |
Track a run
GET https://api.tomorrow.io/v4/data/orders/timelines/{orderId}/runs/{runId}?apikey=<YOUR_API_KEY>
Poll this endpoint until the status is completed, failed, cancelled or skipped. You can read a run for approximately 30 days.
{
"runId": "3f1c2b9e-7a4d-4e8b-9c21-5d6e7f8a9b0c",
"status": "completed",
"outcome": "partial",
"outcomeReason": "insufficient_credits"
}On a completed run, outcome: partial means that some locations are not in the data file, and failures.json lists them. outcomeReason: insufficient_credits means that some of them failed because your account did not have enough credits.
| Status | Meaning |
|---|---|
queued | The run was accepted but has not started. |
running | The run is in progress. |
completed | The file was delivered. If outcome is partial, some locations are not in the file. failures.json lists them. |
failed | The run did not deliver what was ordered. |
cancelling | Returned only in the response to a cancel request. The run shows running until the cancel is complete. |
cancelled | The run was cancelled. |
skipped | The run did not start because another run of the same order was in progress. This applies to scheduled runs and to runs that you start with POST .../runs. |
Manage orders and runs
All paths start with https://api.tomorrow.io/v4 and need the apikey query parameter.
| Request | Description |
|---|---|
GET /data/orders?dataset=timelines | Lists your bulk timelines orders. Use limit (1–200, default 25) and offset to get more pages. You can also filter by status (active or paused). |
GET /data/orders/timelines/{orderId} | Gets an order, its schedule and its most recent run in the last 14 days. |
PATCH /data/orders/timelines/{orderId} | Pauses, resumes or renames an order. Refer to Pause, resume or rename an order. |
DELETE /data/orders/timelines/{orderId} | Deletes the order and its schedule. Runs that are in progress are not cancelled. Cancel them first. Returns 204. |
GET /data/orders/timelines/{orderId}/runs | Lists the runs of the order from the last 14 days, newest first, with a maximum of 50. You can filter with status, and you can repeat this parameter. In this list, a skipped run shows as cancelled. |
POST /data/orders/timelines/{orderId}/runs | Starts a new run now with the stored order. Do not send a body. Relative start and end values are resolved when the run starts. Returns 202 with the new runId. If another run of the order is in progress, the new run is skipped. |
DELETE /data/orders/timelines/{orderId}/runs | Cancels all runs of the order that are not finished. Returns 202 with the lists cancelled and failed. The order continues to run on its schedule. To stop it, pause the order. |
DELETE /data/orders/timelines/{orderId}/runs/{runId} | Cancels one run. Returns 202 with the status cancelling. Data that is already in delivery can still arrive. If the run is already finished, returns 200 with its final state. |
Cancel runs before you delete an orderDeleting an order does not cancel its runs. After an order is deleted, you cannot list its runs, and you can cancel a run only with
DELETE /data/orders/timelines/{orderId}/runs/{runId}. Keep your run IDs.
Pause, resume or rename an order
curl -X PATCH "https://api.tomorrow.io/v4/data/orders/timelines/{orderId}?apikey=$TOMORROW_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "active": false }'{"active": false}pauses a scheduled order. Future runs do not start. A run that is in progress continues.{"active": true}resumes a scheduled order.{"name": "new-name"}renames the order.
An order that you created with "active": false must be activated with {"active": true} before you can change anything else. Until then, other changes return 409 run_pending. When you activate it, an on-demand order starts its run, and a scheduled order starts to run at its next tick.
To change the locations, fields, time window or schedule, create a new order.
Limits
| Limit | Error |
|---|---|
| A maximum of 50,000 locations in one order. An order with more locations is refused, not shortened. For a feed, this is checked when the run starts. | 422 too_many_locations (on create) or error.json (for a feed) |
| A maximum of 100 fields in one order. | 422 too_many_fields |
| A maximum of 10 active orders in one account, for all data order types together. This includes active scheduled orders and on-demand orders whose run is not finished. Paused orders are not counted. | 422 order_limit_exceeded |
| A minimum of 5 minutes between scheduled runs. | 422 invalid_schedule |
Errors
All error responses from the API have the same structure:
{
"code": "too_many_locations",
"field": "locations",
"limit": 50000,
"message": "..."
}Use code in your logic, because its values do not change. field is the request field that caused the error, and limit is the limit that was exceeded or the list of valid values. Not all errors have field and limit.
| Status | Meaning |
|---|---|
400 | The request is not valid. invalid_body (unknown parameter or JSON that is not valid), missing_field, conflicting_fields (both locations and feedId), invalid_field (for example a time, timestep, bucket name, or a field or timezone that the Timelines API refuses), invalid_format, invalid_units, invalid_destination_type, invalid_trigger_type. |
401 | The API key is missing or not valid. |
404 | The order or run does not exist. |
409 | name_conflict (the name is already used), run_pending (the order was created inactive and is not activated). |
422 | The request is valid but cannot be done. Codes: missing_credential, invalid_locations, location_not_found, feed_not_found, too_many_locations, too_many_fields, order_limit_exceeded, invalid_schedule. |
500 / 502 / 503 | A temporary error on our side. Send the request again. |