Bulk Timelines

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:

  1. Set up your bucket. Contact Tomorrow.io for setting credentials. Refer to Destination.
  2. Create an order with your locations (or a assets feed), fields, time window, format and destination.
  3. Track the run until it is complete.
  4. Get the files from your bucket.
📘

Orders and runs

An 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

ParameterTypeRequiredDescription
locationsarrayOne of locations or feedIdThe 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.
feedIdstringOne of locations or feedIdThe 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.
fieldsarray of stringsYesThe fields to export from our available data layers. The maximum is 100 fields.
formatstringYesThe format of the delivered file: csv, json or jsonl. Refer to Data file. Any other value returns 400 invalid_format.
destinationobjectYesThe bucket that receives the file. Refer to Destination.
timestepsarray of stringsNoOne or more timesteps. Refer to Timestep Availability.
startstringNoThe start of the time window: an ISO 8601 time, now, nowPlusXm/h/d or nowMinusXm/h/d (for example nowPlus6h).
endstringNoThe end of the time window. It accepts the same formats as start. If end is before start, the request returns 400 invalid_field.
timezonestringNoAn 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.
dailyStartHourintegerNoThis 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.
unitsstringNometric or imperial. Default is metric. Refer to Units for additional explanation. Any other value returns 400 invalid_units.
namestringNoA 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.
activebooleanNoThe 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.
triggerobjectNoOmit 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 start

Set 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" }
ParameterDescription
typegcs, s3 or azure. Any other value returns 400 invalid_destination_type.
bucketThe bucket name only. A value with gs://, /, : or spaces returns 400 invalid_field.
prefixOptional. 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_schedule with "limit": "5m0s".
📘

Relative time windows

On a scheduled order, relative values such as nowPlus2d is 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>/.

FileWhen it is writtenContents
timelines.<format>When at least one location succeededThe weather data. Refer to Data file.
failures.jsonWhen at least one location failedThe 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.jsonWhen at least one location succeededHow many credits were charged for the run. Refer to credits.json.
error.jsonWhen the whole run was refused, or when all locations failed with the same query errororder_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
  • location is the location as you sent it: the [lat,lon] pair or "lat, lon" string, or the GeoJSON as compact JSON.
  • id is the ID of a saved location (then location is empty), or the feed asset's key for a feed. For inline locations, id is 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.

reasonMeaning
location_not_foundThe location was not found.
invalid_requestThe Timelines API refused the request for this location.
not_entitledYour plan does not include the data that was requested.
rate_limitedThe request was rate limited.
insufficient_creditsYour account did not have enough credits.
upstream_unavailableA temporary error on our side.
unknownAny other error, for example no data for the location.

credits.json

KeyDescription
order_id, run_idThe order and the run.
credits_usedThe credits that the run used. Failed locations are not charged.
billed_callsThe number of billed calls.
completefalse if part of the run did not report its credits. Then credits_used can be too low.
balance_remainingYour remaining Timelines credit balance when the file was written.
locations_total, locations_ok, locations_failedThe number of locations in total, that succeeded, and that failed.
locations_insufficient_creditsThe 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

codeMeaning
permission_deniedYour identity was refused.
feed_not_foundThe feed does not exist, is empty, or you do not have access to it.
too_many_locationsThe feed has more than 50,000 locations.
invalid_orderThe order is not valid.
invalid_queryThe 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.

StatusMeaning
queuedThe run was accepted but has not started.
runningThe run is in progress.
completedThe file was delivered. If outcome is partial, some locations are not in the file. failures.json lists them.
failedThe run did not deliver what was ordered.
cancellingReturned only in the response to a cancel request. The run shows running until the cancel is complete.
cancelledThe run was cancelled.
skippedThe 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.

RequestDescription
GET /data/orders?dataset=timelinesLists 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}/runsLists 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}/runsStarts 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}/runsCancels 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 order

Deleting 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

LimitError
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.

StatusMeaning
400The 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.
401The API key is missing or not valid.
404The order or run does not exist.
409name_conflict (the name is already used), run_pending (the order was created inactive and is not activated).
422The 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 / 503A temporary error on our side. Send the request again.