Start here
Your first report, in four requests
Four requests take you from a fresh credential to a finished, citation-backed weather report with a signed link to its PDF. Every sample here is runnable as written once your token is in the environment.
Before the first request
An owner issues API tokens from Settings → API tokens in the application.
Grant this one reports:read, reports:write and
reports:export; the secret starts with wt_ and is shown exactly
once. Put it in your environment as TITAN_API_TOKEN, which is where every sample
on these pages reads it from:
export TITAN_API_TOKEN="wt_9f3kd02m_…"
The API is included from the Professional plan up. If the plan does not include it, every request answers 402 naming the tier that would, so nothing below will fail mysteriously.
1. Say hello
Ask who you are before spending anything. The answer names the token, the plan, and how many reports the period still holds, so a bulk run can check its budget before it starts.
Retrieve the account
GET /api/v1/account
The credential, the plan, and the limits, before anything is spent.
any valid token
Answers
200
The answer.
Plus the shared refusals: 401, 402, 403, 404, 422 and 429, described on Errors.
curl https://titanweather.com/api/v1/account \
-H "Authorization: Bearer $TITAN_API_TOKEN"
$titan = new \GuzzleHttp\Client([
'base_uri' => 'https://titanweather.com',
'headers' => ['Authorization' => 'Bearer '.getenv('TITAN_API_TOKEN')],
]);
$response = $titan->get('/api/v1/account');
$account = json_decode((string) $response->getBody(), true);
const response = await fetch('https://titanweather.com/api/v1/account', {
headers: {
Authorization: `Bearer ${process.env.TITAN_API_TOKEN}`,
},
});
const account = await response.json();
import os
import requests
response = requests.get(
"https://titanweather.com/api/v1/account",
headers={
"Authorization": f"Bearer {os.environ['TITAN_API_TOKEN']}",
},
)
account = response.json()
{
"object": "account",
"team": {
"name": "North Texas Claims Group",
"plan": "professional",
"plan_label": "Professional",
"on_trial": false,
"lapsed": false
},
"limits": {
"lookback_years": 10,
"max_radius_miles": 10,
"monthly_reports": 25,
"reports_used": 7,
"reports_remaining": 18,
"pdf_export": true
},
"credits": {
"balance": 18,
"monthly_allowance": 25,
"used_this_period": 7,
"renews_at": "2026-09-01T00:00:00Z"
},
"token": {
"name": "Claims intake",
"prefix": "wt_9f3kd02m",
"abilities": [
"reports:read",
"reports:write",
"reports:export"
],
"expires_at": null,
"last_used_at": "2026-08-23T14:02:11Z"
}
}
2. Ask for a report
One address, one period. The answer is 202, not 201: the report exists but is
not yet generated, because compiling it reads the federal archive. The
Idempotency-Key header makes the request safe to retry; a timeout can never bill
you for a second report.
Create a report
POST /api/v1/reports
Ask for a report. Answers 202; poll links.self for the result.
reports:write Idempotency-Key honoured
Body
address
string
required
The property address, as a person would write it. Located for you; the response says where it resolved.
window_start
string
required
The first day of the period to search, YYYY-MM-DD. Must be in the past.
window_end
string
required
The last day of the period, on or after window_start.
radius_miles
number
How far around the address to search, 1 to 100. The plan may narrow it; window.narrowed_by_plan says so.
reference
string
Your own file or claim number, carried on the report unchanged.
recipient
string
Who the document is prepared for, printed on the cover.
property_id
string
A saved property to file this report under.
layout
array
Which sections, imagery and map layers the document is composed with. Requires a plan that can compose layouts; otherwise a 402 names the tier.
Answers
200
Replayed. This Idempotency-Key was honoured before; the earlier answer is returned again, with an Idempotent-Replay: true header.
202
Accepted. The report exists but is not yet generated; poll links.self for the result.
409
This Idempotency-Key was already used for a different request, or its first request is still being processed. A retry must resend the request it is retrying.
Plus the shared refusals: 401, 402, 403, 404, 422 and 429, described on Errors.
curl -X POST https://titanweather.com/api/v1/reports \
-H "Authorization: Bearer $TITAN_API_TOKEN" \
-H "Idempotency-Key: claim-48211-first-report" \
-H "Content-Type: application/json" \
-d '{
"address": "1428 Cedar Ridge Dr, Plano, TX 75023",
"window_start": "2016-08-23",
"window_end": "2026-08-23",
"radius_miles": 5
}'
$titan = new \GuzzleHttp\Client([
'base_uri' => 'https://titanweather.com',
'headers' => ['Authorization' => 'Bearer '.getenv('TITAN_API_TOKEN')],
]);
$response = $titan->post('/api/v1/reports', [
'headers' => ['Idempotency-Key' => 'claim-48211-first-report'],
'json' => [
'address' => '1428 Cedar Ridge Dr, Plano, TX 75023',
'window_start' => '2016-08-23',
'window_end' => '2026-08-23',
'radius_miles' => 5,
],
]);
$report = json_decode((string) $response->getBody(), true);
const response = await fetch('https://titanweather.com/api/v1/reports', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TITAN_API_TOKEN}`,
'Idempotency-Key': 'claim-48211-first-report',
'Content-Type': 'application/json',
},
body: JSON.stringify({
address: '1428 Cedar Ridge Dr, Plano, TX 75023',
window_start: '2016-08-23',
window_end: '2026-08-23',
radius_miles: 5,
}),
});
const report = await response.json();
import os
import requests
response = requests.post(
"https://titanweather.com/api/v1/reports",
headers={
"Authorization": f"Bearer {os.environ['TITAN_API_TOKEN']}",
"Idempotency-Key": "claim-48211-first-report",
},
json={
"address": "1428 Cedar Ridge Dr, Plano, TX 75023",
"window_start": "2016-08-23",
"window_end": "2026-08-23",
"radius_miles": 5,
},
)
report = response.json()
{
"data": {
"id": "0198b6c1-22e4-7f88-a1c9-5d40b7e2913a",
"object": "report",
"status": "pending",
"address": {
"query": "1428 Cedar Ridge Dr, Plano, TX 75023",
"street": null,
"city": null,
"state": null,
"postal_code": null,
"county": null,
"latitude": null,
"longitude": null
},
"window": {
"start": "2016-08-23",
"end": "2026-08-23",
"radius_miles": 5,
"narrowed_by_plan": false
},
"property_id": null,
"event_count": 0,
"observation_count": 0,
"coverage": {
"complete": false,
"provisional": false,
"summary": null,
"sources_searched": []
},
"headline": null,
"findings": null,
"failure_reason": null,
"plan_tier": "professional",
"layout": {
"sections": [
"summary",
"findings",
"map",
"events"
],
"imagery": [
"aerial",
"street_view"
],
"elements": [
"key_figures"
],
"map": null
},
"created_at": "2026-08-23T14:02:11Z",
"generated_at": null,
"pdf": {
"status": null,
"rendered_at": null
},
"links": {
"self": "https://titanweather.com/api/v1/reports/0198b6c1-22e4-7f88-a1c9-5d40b7e2913a",
"events": null,
"pdf": null
}
}
}
3. Poll for the result
Follow links.self from the 202 until status is
complete, typically well under a minute. Every key is present from the first
poll; what is not yet earned is null, never missing, so your parser is written once. When the
report finishes, headline and findings carry the answer and
coverage says whether the archive could actually speak to the whole period.
Retrieve a report
GET /api/v1/reports/{report}
One report: its findings, coverage and the shape of its document.
reports:read
Path parameters
report
The report id, a uuid from a create or list response.
Answers
200
The answer.
Plus the shared refusals: 401, 402, 403, 404, 422 and 429, described on Errors.
curl https://titanweather.com/api/v1/reports/0198b6c0-51a9-70f2-8e4d-93b2a7c15f60 \
-H "Authorization: Bearer $TITAN_API_TOKEN"
$titan = new \GuzzleHttp\Client([
'base_uri' => 'https://titanweather.com',
'headers' => ['Authorization' => 'Bearer '.getenv('TITAN_API_TOKEN')],
]);
$response = $titan->get('/api/v1/reports/0198b6c0-51a9-70f2-8e4d-93b2a7c15f60');
$report = json_decode((string) $response->getBody(), true);
const response = await fetch('https://titanweather.com/api/v1/reports/0198b6c0-51a9-70f2-8e4d-93b2a7c15f60', {
headers: {
Authorization: `Bearer ${process.env.TITAN_API_TOKEN}`,
},
});
const report = await response.json();
import os
import requests
response = requests.get(
"https://titanweather.com/api/v1/reports/0198b6c0-51a9-70f2-8e4d-93b2a7c15f60",
headers={
"Authorization": f"Bearer {os.environ['TITAN_API_TOKEN']}",
},
)
report = response.json()
{
"data": {
"id": "0198b6c0-51a9-70f2-8e4d-93b2a7c15f60",
"object": "report",
"status": "complete",
"address": {
"query": "1428 Cedar Ridge Dr, Plano, TX 75023",
"street": "1428 Cedar Ridge Dr",
"city": "Plano",
"state": "TX",
"postal_code": "75023",
"county": "Collin",
"latitude": 33.0412,
"longitude": -96.6996
},
"window": {
"start": "2016-08-23",
"end": "2026-08-23",
"radius_miles": 5,
"narrowed_by_plan": false
},
"property_id": "0198a3f2-6d7e-7b41-92c5-2f6a1c9d4e88",
"event_count": 14,
"observation_count": 41,
"coverage": {
"complete": true,
"provisional": false,
"summary": "All nine sources answered for the full period.",
"sources_searched": [
"noaa_storm_events",
"iem_lsr",
"spc_reports",
"noaa_swdi",
"cocorahs"
]
},
"headline": "Hail to 2.75 in within 1 mile, most recently April 21, 2023.",
"findings": [
{
"family": "hail",
"family_label": "Hail",
"event_count": 9,
"peak_severity": "severe",
"evidence_kinds": [
"ground",
"radar"
],
"evidence_labels": [
"Ground report",
"Radar detection"
],
"dates": [
"2023-04-21",
"2021-04-28",
"2019-06-09"
],
"most_recent": "2023-04-21",
"peak_hail_in": 2.75,
"peak_wind_mph": null,
"peak_tornado_ef": null,
"nearest_miles": 0.8,
"any_observed": true,
"confidence": "high",
"confidence_label": "High confidence",
"statement": "Nine hail events on record, the largest 2.75 in measured 0.8 miles away."
}
],
"fingerprint": "f3b1c9d84a027e65b2f8a1c40d9e73258c6b0a4f1d92e8735a6c410b9f2d8e07",
"failure_reason": null,
"plan_tier": "professional",
"layout": {
"sections": [
"summary",
"findings",
"map",
"events"
],
"imagery": [
"aerial",
"street_view"
],
"elements": [
"key_figures"
],
"map": null
},
"created_at": "2026-08-21T16:44:09Z",
"generated_at": "2026-08-21T16:45:03Z",
"pdf": {
"status": "ready",
"rendered_at": "2026-08-21T16:45:41Z"
},
"links": {
"self": "https://titanweather.com/api/v1/reports/0198b6c0-51a9-70f2-8e4d-93b2a7c15f60",
"events": "https://titanweather.com/api/v1/reports/0198b6c0-51a9-70f2-8e4d-93b2a7c15f60/events",
"pdf": "https://titanweather.com/api/v1/reports/0198b6c0-51a9-70f2-8e4d-93b2a7c15f60/pdf"
}
}
}
4. Take the document away
The PDF is the product: the same document the application renders, byte for byte. Ask for a short-lived signed link and hand it to a browser, a claims system or an email template without proxying megabytes through your own process.
Create a download link
POST /api/v1/reports/{report}/pdf/link
The rendered document, as a short-lived signed link; 202 while it is still being rendered.
reports:export
Path parameters
report
The report id, a uuid from a create or list response.
Answers
200
A short-lived signed link to the rendered document.
202
The document is being rendered in the background. The body carries status: preparing and retry_after; poll this endpoint, or pdf.status on the report.
409
The report has not finished generating, so there is no document yet.
503
The rendering engine is unavailable. An outage of ours, not a billing problem; retry later.
Plus the shared refusals: 401, 402, 403, 404, 422 and 429, described on Errors.
curl -X POST https://titanweather.com/api/v1/reports/0198b6c0-51a9-70f2-8e4d-93b2a7c15f60/pdf/link \
-H "Authorization: Bearer $TITAN_API_TOKEN"
$titan = new \GuzzleHttp\Client([
'base_uri' => 'https://titanweather.com',
'headers' => ['Authorization' => 'Bearer '.getenv('TITAN_API_TOKEN')],
]);
$response = $titan->post('/api/v1/reports/0198b6c0-51a9-70f2-8e4d-93b2a7c15f60/pdf/link');
$download = json_decode((string) $response->getBody(), true);
const response = await fetch('https://titanweather.com/api/v1/reports/0198b6c0-51a9-70f2-8e4d-93b2a7c15f60/pdf/link', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TITAN_API_TOKEN}`,
},
});
const download = await response.json();
import os
import requests
response = requests.post(
"https://titanweather.com/api/v1/reports/0198b6c0-51a9-70f2-8e4d-93b2a7c15f60/pdf/link",
headers={
"Authorization": f"Bearer {os.environ['TITAN_API_TOKEN']}",
},
)
download = response.json()
{
"object": "download",
"url": "https://titanweather.com/storage/reports/0198b6c0-51a9-70f2-8e4d-93b2a7c15f60.pdf?expires=1787841903&signature=1c9d84a0",
"signed": true,
"filename": "titan-weather-plano-tx.pdf",
"content_type": "application/pdf",
"bytes": 1284772,
"expires_at": "2026-08-23T15:05:00Z"
}
Where to next
- The Reports reference, for batches, cited records and the verification statement.
- Webhooks, so a finished report arrives at your endpoint instead of being polled for.
- Properties, to keep a whole book of addresses watched between reports.