Preparing your data
Good forecasts start with clean, consistent input. This guide covers how to get your history into Temporis in a shape it can read well.
Use Unix seconds, consistently
Every timestamp you send is an integer count of Unix seconds — the number of seconds since 1970-01-01 00:00:00 UTC. Not milliseconds, not an ISO date string. If your source system gives you milliseconds, divide by 1000 before sending.
Pick one timezone discipline and never deviate from it. The simplest rule that always works: store everything in UTC. Unix seconds are inherently UTC, so as long as you convert each local timestamp to its true UTC instant before sending, your series stays internally consistent. Mixing local-time and UTC values in the same series silently shifts points around and corrupts the daily pattern the model learns.
Your timestamps do not need to land on round numbers. A data profile's interval snaps observations onto a regular grid — for the hourly_orders profile, every point is bucketed to the nearest hour. You supply the real instant each observation happened; the profile handles the alignment.
In Python, int(datetime(2026, 6, 1, tzinfo=timezone.utc).timestamp()) gives you the Unix seconds for an explicit UTC instant. Always attach a timezone so the conversion is unambiguous.
Send history in batches
You rarely have one observation to send — you have months or years of it. The ingest endpoint takes many records per request, so chunk your backfill into batches of a few thousand records and post them in a loop rather than one record at a time.
Ingestion is idempotent. The upsert key is (timestamp, name), so re-sending a record you already sent simply overwrites it with the same value — a no-op. That means a backfill is safe to re-run: if a batch fails halfway or your script crashes, just start it again. You will not create duplicates.
import os, requests
token = os.environ["TEMPORIS_TOKEN"]
BASE = "https://api.temporis.co"
# history is a list of (unix_seconds, value) pairs, oldest first
def records_for(history):
return [{"timestamp": ts, "name": "orders", "value": v} for ts, v in history]
def chunked(seq, size):
for i in range(0, len(seq), size):
yield seq[i:i + size]
records = records_for(history)
for batch in chunked(records, 2000):
resp = requests.post(
f"{BASE}/v1/data_sources/ingest",
headers={"Authorization": f"Bearer {token}"},
json={"data_source": "store_metrics", "records": batch},
)
resp.raise_for_status() # 204 No Content on success
print(f"ingested {len(records)} records")A successful ingest returns HTTP 204 No Content with an empty body, so there is nothing to parse — just confirm the status. See the ingest reference for the full request shape.
Give the model enough history
A profile cannot serve predictions until it is ready — Temporis needs enough history to compute the normalization statistics it reads your series through. And each prediction needs at least min_patches worth of recent context, or the request returns 409 "Not enough data for this data profile."
The practical rule: when in doubt, send more history. More history lets a profile become ready, comfortably clears min_patches, and gives the model more seasonal cycles to learn from. There is no penalty for sending more than the context window uses — older points beyond num_patches simply aren't read at predict time.
To learn what min_patches, num_patches, and "ready" mean and how they relate to your interval, read Data profiles.
Irregular timestamps and gaps
Real data is messy. Sensors skip a reading, a job runs a few minutes late, a store is closed on a holiday. You do not need to clean any of this by hand — the profile's resampling step handles it.
The interval buckets your observations onto a regular grid, which absorbs slightly irregular timestamps: a reading that arrives at 10:58 and one at 11:03 both land in the 11:00 bucket. For missing buckets, fill_limit sets how many consecutive empty buckets Temporis will forward-fill before it gives up. A short gap is bridged; a long gap is left as a hole and those rows are dropped rather than invented.
If your series has recurring dead time — hours when nothing can possibly happen — don't treat it as missing data at all. An exclude_window masks that time-of-day range so it never distorts the series. That's a profile setting, covered in Data profiles.
You send the raw observations as you have them. Where they land, how gaps are filled, and what dead time is ignored are decisions the profile makes — not transforms you bake into the data.
Aligning multiple series
A profile can read several series at once — a multivariate profile that forecasts them jointly. For that to work well, the series need a common timeline: send each series' observation at the same set of timestamps so the rows line up after bucketing.
You can send multiple series in a single ingest request — just include a record per series at each timestamp. Here two series, orders and visitors, share the same timeline:
{
"data_source": "store_metrics",
"records": [
{ "timestamp": 1718841600, "name": "orders", "value": 42 },
{ "timestamp": 1718841600, "name": "visitors", "value": 530 },
{ "timestamp": 1718845200, "name": "orders", "value": 39 },
{ "timestamp": 1718845200, "name": "visitors", "value": 504 },
{ "timestamp": 1718848800, "name": "orders", "value": 51 },
{ "timestamp": 1718848800, "name": "visitors", "value": 612 }
]
}If one series is missing at a timestamp where another is present, that bucket is filled per fill_limit just like any other gap — so a few stragglers won't break alignment, but you'll get the cleanest joint forecast when the series move on the same clock.
Corrections and backfills
Found a wrong value? Restating a quarter? You don't delete or patch anything special — you just re-send the corrected record. Because the upsert key is (timestamp, name), sending a new value at an existing timestamp+name overwrites the old one.
{
"data_source": "store_metrics",
"records": [
{ "timestamp": 1718841600, "name": "orders", "value": 47 }
]
}Because every ingest is an idempotent upsert, a corrected backfill can safely overlap data you already sent. Re-post the whole affected range if it's easier than computing exactly which points changed.