Tripomatic Partner API
Travel alerts for a place, a webhook when an alert touches an itinerary you registered, government advisories, and public holidays and events.
https://api.tripomatic.comThe machine-readable description of everything on this page is the OpenAPI 3.1 file at /docs/openapi.yaml. Names, ids and texts in the examples are illustrative.
What this API is#
The API passes on what official sources publish, in one format. It tells you which alerts apply to a place or an itinerary, how serious they are and when they change.
- Structured facts you can build on. Each alert comes with its type, severity, time and area, and a lookup adds the reason it matches your place. You decide which alerts to show, to whom and in what words, so they fit naturally into your product and your communication with travellers.
- Alert geometry. Issuers often publish only zone codes or place names. We resolve them into areas you can use: named places, standard region codes and, for many alerts, a GeoJSON shape with a bounding box, ready to draw on a map. Your place is tested against the area, so
matchtells you whether it lies inside, is partly covered or is close by. - Webhooks for your itineraries. Register an itinerary once and we match it against new and changed alerts. Your server gets a signed webhook when an alert touches an itinerary, changes or ends, with the ids of the itineraries it concerns, so you do not have to keep asking.
- Continuously updated. We check every source around the clock and deliver new and updated alerts shortly after they are published, through the API or by webhook. An empty list means there is no alert we know of for that place. Every alert names its issuing authority, with a link to the authority or the original notice where one is available. The authority remains the reference for what is in force.
- The original text, plus a summary. An alert's text is the issuer's own words in the original language, so the message stays exactly as the authority wrote it. When you need something ready to show, the summary gives you a short machine-written version in the language you ask for, from its own endpoint, so it never mixes with the issuer's text.
Getting started#
- Sign in at customer.tripomatic.com/partners, create your partner account and create an API key. The key is shown once; store it like a password.
- Look up the alerts at a place:
curl -H "Authorization: Bearer $TRIPOMATIC_KEY" \ "https://api.tripomatic.com/v1/alerts?lat=41.39&lon=2.17" - To be told when alerts touch your bookings, register itineraries (below), set a webhook URL in the same portal, and send yourself a test with POST
/v1/test/alerts.
A new partner account starts on a free plan, so you can try everything before you decide.
Authentication#
Send your key in the Authorization header of every request:
Authorization: Bearer tk_<partnerId>_<keyId>_<secret>- Call the API from your servers, never from a browser or an app: a key in a client can be read by anyone.
- You can hold several keys at once, so you can rotate without downtime: create the new key, deploy it, revoke the old one. A revoked or expired key stops working within a minute.
- A missing, unknown, revoked or expired key all answer 401
invalid_key, deliberately without saying which. - The keys start with
tk_so that secret scanners recognise one if it leaks. If it does, revoke it in the portal.
This page and the OpenAPI file need no key.
Conventions and versioning#
- Versioning. The path carries the major version:
/v1. Within a major version, changes only add: new fields, new endpoints, new values of an enumeration (kinds, severities, match types, event names, error codes, failure reasons). Ignore fields and values you do not know and do not fail on them. Anything that removes or changes a meaning would be a new major version. - Times are UTC ISO-8601 with a
Z. Distances are metric. Calendar dates areYYYY-MM-DD. - Countries. Where an endpoint takes
iso, it accepts ISO 3166-1 alpha-2 or alpha-3 country codes and, if its entry says so, ISO 3166-2 region codes (DE-BY), in any case. Responses use alpha-3. - Coordinates are decimal degrees:
lat=41.39&lon=2.17. We do not need a precise position. To avoid passing on a user's exact location, round to two decimal places. We never round on our side, and we never log coordinates or query strings. - Request id. Every response has an
X-Request-Idheader, and every error body repeats it asrequestId. Quote it when you contact us. - Caching. Responses are
Cache-Control: private, no-store, except a complete summary and the list of locales, which may be kept for an hour. Ask again when you need current data.
Errors#
Every error has the same shape. Program against code; the message is for people and may change.
{
"error": {
"code": "quota_exceeded",
"message": "Monthly quota reached.",
"requestId": "3f9c2a7e-6d1b-4c58-9a40-2b7e5d1c8f03"
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A parameter or the body is malformed or missing. The message never repeats what you sent. |
| 401 | invalid_key | The key is missing, unknown, revoked or expired. |
| 403 | partner_suspended | Your account is suspended. |
| 403 | free_plan_expired | Your free plan has ended. |
| 404 | not_found | No such alert, itinerary, country or region for you, or no such route. An alert we cannot pass on to partners is also not_found. |
| 405 | method_not_allowed | The route does not take this method. |
| 409 | webhook_not_configured | You asked for a test webhook, and no webhook is set up. |
| 429 | rate_limited | Too many calls in a short window: a rate limit. Has Retry-After. The call does not count against your quota. |
| 429 | quota_exceeded | A monthly quota is used up. Has Retry-After, the seconds until it resets on the 1st at 00:00 UTC. |
| 500 | internal_error | A bug on our side. Quote the requestId. |
| 503 | upstream_unavailable | A service we depend on failed. It is never returned as an empty result: an empty list always means there is nothing. Retry with a delay. |
Retry 429 and 503 after the Retry-After wait, or with exponential backoff and jitter if there is none. Do not retry 400, 401 or 403 unchanged.
Pagination#
Lists of alerts and of itineraries are paged. Pass limit (1 to 50, default 25) and cursor. A page answers nextCursor until the last page, where it is absent. Send the cursor back with the same other parameters. A cursor is opaque: do not build, change or keep one for later.
GET /v1/alerts?iso=ESP&limit=50
GET /v1/alerts?iso=ESP&limit=50&cursor=<nextCursor>The page also holds total, returned and truncated. When truncated is true, the search for the place stopped early (a very busy area): total is a lower bound and an alert may be missing. Treat such an answer as incomplete, not as the full picture; a narrower lookup (a point instead of a country) usually avoids it.
Rate limits, quotas and the free plan#
Rate limits#
A rate limit caps how many calls you can make in a short window, with ceilings that depend on your plan. It exists to stop runaway loops, not to meter you. Over it you get 429 rate_limited with a Retry-After header: wait that long and back off. A rate-limited call is not counted against your quota. If your calls are repeatedly rate limited, we email the contact address of your partner account.
Quotas#
Your plan has one quota for each of two things: requests and itinerary days. A quota is rated over a UTC calendar month: it resets on the 1st at 00:00 UTC. A summary counts as 10 requests, every other counted call as 1; itinerary days are explained with itineraries. GET /v1/usage shows your quotas and what you have used.
- On the free plan a quota is a stop. When your requests are used up, every call answers 429
quota_exceededwithRetry-After, the seconds until the 1st, when it lifts by itself. When your itinerary days are used up, only registering or extending an itinerary is refused. - On paid plans a slight, temporary overage is acceptable: we notify you and service continues. A sustained overage requires upgrading your plan. Far above the quota, calls are refused with
quota_exceededas on the free plan.
We email the contact address of your partner account when you reach 80% of a quota and when you pass it. Usage is totalled about once an hour, so a quota can be passed by up to an hour of traffic before it takes effect. Webhooks for itineraries you already registered continue when a quota is used up.
What counts#
- Only successful (2xx) requests count.
- These are free:
POST /v1/test/alerts,GET /v1/usageandGET /v1/localesnever count. The last two also keep answering when a quota is used up or the free plan has ended. - Deleting an itinerary is never refused, so you can always erase what you registered.
The free plan#
A new partner account starts on a free plan, so you can try everything before you decide. When it ends, calls answer 403 free_plan_expired and webhooks stop. Your keys and your data are kept, and GET /v1/usage still answers.
Endpoints#
| Endpoint | Purpose |
|---|---|
GET /v1/alerts | Alerts in force at a point, in a country or in a region |
GET /v1/alerts/summary | The alerts of a place or of one leg of an itinerary, compiled into a few cards |
GET /v1/alerts/{id} | One alert with its revisions and geometry |
GET /v1/advisories | Government travel advisories of a country |
GET /v1/locales | The values locale takes |
GET /v1/calendar | Holidays, observances and events |
GET /v1/itineraries | List your itineraries |
PUT /v1/itineraries/{id} | Register or replace an itinerary |
GET /v1/itineraries/{id} | Read an itinerary back |
DELETE /v1/itineraries/{id} | Remove an itinerary |
GET /v1/itineraries/{id}/alerts | Alerts that match an itinerary now |
POST /v1/test/alerts | Send a test webhook to yourself |
GET /v1/usage | Your daily usage and quotas |
GET /docs, GET /docs/openapi.yaml | This page and the OpenAPI file; no key needed |
GET/v1/alerts#
Everything still valid for the place, including alerts that are known but have not started yet. Alerts take no date range.
| Parameter | |
|---|---|
lat, lon | A point, as decimal degrees: lat=41.39&lon=2.17. Give both. We do not need a precise position: to avoid passing on a user's exact location, round to two decimal places. |
iso | Instead of a point: a country (ISO 3166-1 alpha-2 or alpha-3, DE or DEU) or a region (ISO 3166-2, DE-BY). A region we do not know yet is 404, like an unknown code; use the country or a point instead. |
limit, cursor | Paging. |
Give a point or iso, not both. Each alert is in the alert format with a match. Persistent country advisories are not in this list; they are in /v1/advisories.
GET /v1/alerts?lat=41.39&lon=2.17{
"alerts": [
{
"id": "meteoalarm-de-8c1f3a92",
"test": false,
"revision": "8f2c1a",
"status": "active",
"kind": "weather",
"subKind": "storm",
"severity": "severe",
"severityScore": 43,
"start": "2026-11-03T12:00:00Z",
"end": "2026-11-04T06:00:00Z",
"openEnded": false,
"issuedAt": "2026-11-03T09:30:00Z",
"source": { "id": "meteoalarm-de", "name": "Meteoalarm: Germany", "attribution": "[Deutscher Wetterdienst (DWD)](https://www.dwd.de), via [EUMETNET - MeteoAlarm](https://www.meteoalarm.org)" },
"hasGeometry": false,
"area": {
"generalized": false,
"parts": [
{ "type": "place", "code": "DE-BY", "codeSystem": "ISO3166-2", "name": "Bayern" }
]
},
"match": { "type": "exact", "explanation": "The place lies in the alert area" }
}
],
"total": 1,
"returned": 1,
"truncated": false
}Shown with the fields needed to read it; the full object is in the alert format. nextCursor is added until the last page.
Reading a country or a region (iso). The answer holds every alert that really overlaps the place, however small the overlap, except the national warnings of another country. The alerts are sorted by severity and paged. The answer for a place can be up to about 60 seconds old. When truncated is true, alerts may be missing.
GET/v1/alerts/summary#
The alerts of a place, grouped by kind into a few cards of text. A language model writes the text from the issuers' own words, in the language you ask for. There is one summary endpoint, for two uses:
- A place or destination view. Dozens of alerts for a country or a city become a few cards, for a page (
display) or a notification (notification). - One leg of an itinerary. Ask once per leg, with its point and dates (
fromandto), so text is only written for what somebody looks at. A summary for a leg holds the alert a webhook reported for that leg.
| Parameter | |
|---|---|
lat, lon | A point (lat=41.39&lon=2.17), or instead iso. We do not need a precise position: to avoid passing on a user's exact location, round to two decimal places. |
iso | A country (ISO 3166-1 alpha-2 or alpha-3) or a region (ISO 3166-2), as for /v1/alerts. |
locale | Required. A language (es) or a language and a region (es-ES), from the list; anything else is 400. It sets the language of the text, and units and date formats follow it: a bare language gets metric units and 24-hour time. |
timeZone | Optional, an IANA name such as Europe/Madrid. Times in the text are shown in it. The default is the local time of the area; UTC is used only when no zone is known. Times are labelled with their zone, as in "hora de Barcelona". |
from, to | Both or neither: instants with a zone (2026-12-01T10:00:00Z) that select the alerts in force at some point of the window, which may be at most 82 days. Without them, it is everything still valid. |
GET /v1/alerts/summary?lat=41.39&lon=2.17&locale=es-ES{
"groups": [
{
"kind": "weather",
"severity": "severe",
"start": "2026-11-05T13:00:00Z",
"end": "2026-11-05T22:59:00Z",
"openEnded": false,
"display": {
"title": "Aviso naranja por tormentas en Barcelona",
"body": "La Agencia Estatal de Meteorología mantiene el aviso naranja por tormentas en el litoral y el prelitoral de la provincia de Barcelona. Se esperan chubascos fuertes, con hasta 40 l/m² en una hora, y rachas de viento de hasta 70 km/h. El aviso rige el jueves 5 de noviembre, de las 14:00 a las 23:59 (hora de Barcelona)."
},
"notification": {
"title": "Aviso naranja por tormentas fuertes en Barcelona",
"body": "Chubascos fuertes (hasta 40 l/m² en una hora) y rachas de viento de hasta 70 km/h el jueves 5 de noviembre, de 14:00 a 23:59 (hora de Barcelona)."
},
"alertIds": ["meteoalarm-es-4d7a21c8", "meteoalarm-es-90be5f13"],
"alertCount": 2,
"sources": [{ "id": "meteoalarm-es", "name": "Meteoalarm: Spain", "attribution": "[AEMET](https://www.aemet.es), via [EUMETNET - MeteoAlarm](https://www.meteoalarm.org)", "issuedAt": "2026-11-04T16:10:00Z", "displayNotes": "Warnings issued by Agencia Estatal de Meteorología (AEMET). Information covering more than one country: EUMETNET – MeteoAlarm. Time delays between this website and the www.meteoalarm.org website are possible. For the most up-to-date awareness information as published by the participating National Meteorological and Hydrological Services, please refer to www.meteoalarm.org." }],
"revision": "r_3b9e70",
"pending": false
}
],
"truncated": false
}- A group's
alertIdslists every alert it covers, andsourcesthe issuers. The text is machine-written: say so where you show it, and show next to it each source'sname,attribution,issuedAt(the latest among that source's alerts in the group) anddisplayNoteswhen present (attribution). A group has noendif any member alert is open-ended. The cards are indisplayandnotification; whether to send a notification is your decision. pending. A call can take up to about 30 seconds when text has to be written, so set a client timeout of 35 seconds. A group whose text is not finished comes back as the most severe alert's own words with"pending": true: repeat the call to pick up the finished text.truncated. Whentrue, the search stopped early and an alert may be missing, as for lists.revisionof a group changes when its content changes. Compare it for equality only, for example to know a card needs no redraw.- Caching. A complete answer (at least one group, none
pending,truncatedfalse) carriesCache-Control: private, max-age=3600, so you may keep it for an hour; any other answer isno-store. - A complete answer counts as 10 requests; any other 200 counts as 1.
GET/v1/alerts/{id}#
One alert in the alert format, plus its revisions and the geometry of its shapes with their geometryOrigin. Lists and webhooks never carry a polygon: they have the bbox of a shape and the alert's hasGeometry. When it is true, read the polygon here. An alert we cannot pass on is 404.
GET /v1/alerts/meteoalarm-de-8c1f3a92GET/v1/advisories#
The government travel advisories that apply to a country, merged into one entry. iso is a country, ISO 3166-1 alpha-2 or alpha-3 (a region is 400). locale is optional (default en, from the list) and timeZone is optional, both as for the summary; without timeZone, times use the country's zone when it has exactly one, otherwise UTC. Advisories change slowly and are never sent by webhook: poll once or twice a day. The call counts as 1 request.
GET /v1/advisories?iso=ESP&locale=en-GB{
"country": "ESP",
"highestSeverity": "moderate",
"summary": {
"origin": "machine",
"language": "en",
"title": "Travel advice for Spain",
"text": "One issuer advises exercising increased caution because of pickpocketing in tourist areas.",
"pending": false
},
"advisories": [
{
"id": "advisory-esp-1",
"source": {
"id": "example-issuer",
"name": "Example Foreign Ministry",
"attribution": "Source: [Example Foreign Ministry](https://example.org)"
},
"issuer": "Example Foreign Ministry",
"levelNative": "Level 2",
"levelCommon": 2,
"title": "Spain: exercise increased caution",
"statement": "Exercise increased caution because of pickpocketing in tourist areas.",
"statementOrigin": "machine",
"sourceUrl": "https://example.org/advisories/spain"
}
]
}- Each advisory carries the issuer's own
title,levelNativeandsourceUrl. Show the issuer's level and source, not only the merged text.source.attributionis Markdown, as for alerts. originandstatementOriginsay how a text was made:machine(written by a language model from the issuers' words) ortemplate(the issuers' words put together by rule).pending. The text is written on the first read in a language, which can take up to about 30 seconds (use the same 35-second client timeout as for the summary). If it is not finished in time, you get thetemplatewith"pending": true: ask again later.- A country with no advisory answers 200 with
"summary": nulland"advisories": []. A country we cannot read completely is 503.
GET/v1/locales#
The values locale takes: a language (es) or a language and a region (es-ES). A bare language gets metric units and 24-hour time; a region sets units and time format for its country. The list is free (see what counts) and may be cached for an hour.
{ "languages": ["ar", "cs", "da", "..."], "regions": ["AD", "AE", "AF", "..."] }languages are ISO 639-1 codes; regions are ISO 3166-1 alpha-2 country codes. Today the languages are: ar cs da de el en es fi fr he hu it ja ko nb nl pl pt ro ru sk sv tr uk zh. The answer is the authority: more can be added.
GET/v1/calendar#
Public holidays, observances, festivals, closures, seasons and events for a place. Give lat and lon (lat=50.09&lon=14.42; we do not need a precise position, so to avoid passing on a user's exact location, round to two decimal places), or iso, a country (ISO 3166-1 alpha-2 or alpha-3) or a region (ISO 3166-2). from and to are required, inclusive, and may cover at most 31 days. lang (a language tag such as cs) picks the language of the names, falling back to the base language, then English. A region answer holds the country-wide records and the ones that name the region. A point in no known place answers an empty days.
GET /v1/calendar?iso=CZ&from=2026-12-24&to=2026-12-26{
"from": "2026-12-24",
"to": "2026-12-26",
"days": [
{
"id": "christmas_day:2026:CZE",
"seriesId": "christmas_day",
"kind": "public_holiday",
"name": "Christmas Day",
"startDate": "2026-12-25",
"endDate": "2026-12-25",
"effects": ["shops_closed"],
"status": "confirmed",
"scope": { "countryISO": "CZE" }
}
]
}status is projected, announced, confirmed, changed or cancelled; a record may carry dateUncertaintyDays when its date can still move, and an event has an eventCategory. The calendar is separate from alerts and is never sent by webhook.
Itineraries#
An itinerary is a list of legs, each a point and the time window a traveller is there. Register itineraries and we match them against alerts: you get a webhook when an alert touches one. An itinerary holds only coordinates and times.
- Span. An itinerary spans at most 82 days, from its first
fromto its lastto: enough to go around the world in 80, with two to spare. The lasttois at most 366 days ahead and at most one day in the past; earlier legs of a trip in progress may be older, so you can repeat the wholePUTduring the trip. - Itinerary days are what the itinerary quota counts. One itinerary monitored on one UTC day is one itinerary day, over its span from the first
fromto the lastto, gaps included. A day counts in the month you register it (a trip in January registered in November counts in November) and is never given back. A day already counted on an id is not counted again, so repeating or correcting aPUTis free. Days in the past are not counted. - Matches arrive at once. An itinerary that matches an alert right away gets its webhook right away, as
created, or asupdatedif you were already told about that alert for another itinerary.
GET/v1/itineraries#
Your itineraries by id, one page at a time (limit, cursor, paging), each with its first and last day, updatedAt and expiresAt and without its legs; read one with GET /v1/itineraries/{id}. A page can be short, or even empty, and still have a nextCursor: keep going until there is none.
{
"itineraries": [
{
"itineraryId": "booking-123",
"firstDate": "2026-12-01",
"lastDate": "2026-12-04",
"updatedAt": "2026-11-03T09:00:00Z",
"expiresAt": "2027-01-03T09:00:00Z"
}
],
"nextCursor": "..."
}PUT/v1/itineraries/{id}#
PUT /v1/itineraries/booking-123
Content-Type: application/json
{
"legs": [
{
"lat": 50.09,
"lon": 14.42,
"from": "2026-12-01T10:00:00Z",
"to": "2026-12-04T09:00:00Z"
}
]
}200 OK
{
"itineraryId": "booking-123",
"changed": true,
"expiresAt": "2027-01-03T09:00:00Z"
}- The id is yours: 1 to 128 characters of letters, digits,
_and-, unique among your itineraries. Do not put anything personal in it. PUTreplaces the whole itinerary and is safe to repeat;changedisfalsewhen the legs were already as sent.- Legs. 1 to 100 legs.
latandlonare decimal degrees; we do not need a precise position, so to avoid passing on a user's exact location, round to two decimal places.fromis beforeto(which is exclusive), both instants with a zone. The legs may cover at most 1,000 days in all, counting one per leg and UTC day. The body is at most 128 KB. The span rules apply. - Deleted automatically 30 days after the end of the last leg (
expiresAt). Re-register to keep one longer. - New itinerary days count against your itinerary quota. When it is used up, the answer is 429
quota_exceededand nothing is stored; aPUTthat adds no new day is never refused. - If storing fails the answer is 503: send the same
PUTagain. It finishes the job and does not count twice. - Alerts that already match are delivered as webhooks soon after registration.
GET/v1/itineraries/{id}#
200 with itineraryId, legs as registered, updatedAt and expiresAt; 404 for an id you have not registered, or that has been deleted.
{
"itineraryId": "booking-123",
"legs": [
{
"lat": 50.09,
"lon": 14.42,
"from": "2026-12-01T10:00:00Z",
"to": "2026-12-04T09:00:00Z"
}
],
"updatedAt": "2026-11-03T09:00:00Z",
"expiresAt": "2027-01-03T09:00:00Z"
}DELETE/v1/itineraries/{id}#
204. It stops matching. A deleted itinerary gets no ended webhook of its own. Deleting is never refused for a quota or after the free plan has ended.
GET/v1/itineraries/{id}/alerts#
The alerts that match the itinerary now, in the alert format with a match that also lists the legs it concerns (indexes into the legs as registered), paged like /v1/alerts. Use it to catch up after your endpoint was down, and to check state at any time. While the itinerary has not been matched completely, or a leg cannot be read, the answer is 503, never a short list.
POST/v1/test/alerts#
Sends one synthetic alert ("test": true, "event": "created") to your webhook, signed like a real one, and answers the outcome of that attempt. It goes only to you, is never stored, does not count as usage, and is limited to 100 an hour. One attempt, no retry. A webhook that is switched off still receives it, so you can test before switching on.
POST /v1/test/alerts
Content-Type: application/json
{
"itineraryIds": ["booking-123"]
}itineraryIds is optional, at most 10; ids that are not yours are left out. They are listed in the webhook's itineraries with "legs": [0].
200 OK
{
"deliveryId": "dlv_test_4d2a9c10",
"delivered": true,
"statusCode": 204,
"durationMs": 182,
"itineraryIds": ["booking-123"],
"webhookEnabled": false
}The answer is 200 also when your endpoint failed: read delivered, statusCode and failure. failure is one of refused_url, refused_address, unresolved, timeout, connection_failed, redirect_refused, http_status. With no webhook set up the answer is 409 webhook_not_configured; over the limit, 429 rate_limited. The test alert's source.id is tripomatic-test, so it cannot be mistaken for a real alert.
GET/v1/usage#
Your daily totals of what counts against your plan, with your plan and quotas. from and to are required UTC dates, YYYY-MM-DD, inclusive, at most 92 days. The figures are updated about once an hour.
GET /v1/usage?from=2026-11-01&to=2026-11-02{
"from": "2026-11-01",
"to": "2026-11-02",
"days": [
{ "date": "2026-11-01", "requests": 412, "itineraryDays": 9, "summaries": 3, "webhooks": 17 },
{ "date": "2026-11-02", "requests": 0, "itineraryDays": 0, "summaries": 0, "webhooks": 0 }
],
"totals": { "requests": 412, "itineraryDays": 9, "summaries": 3, "webhooks": 17 },
"quota": { "requestsPerMonth": 1000, "itineraryDaysPerMonth": 350 },
"plan": { "type": "free", "freeUntil": "2026-12-01T00:00:00Z" }
}- Every day of the range is listed, with zeros for a day without usage.
requests: successful requests, a summary counting 10.itineraryDays: itinerary days counted.summaries: summaries served, for information; they are already inrequests.webhooks: webhooks your endpoint acknowledged, test alerts excluded.quotaholds your monthly quotas:requestsPerMonthanditineraryDaysPerMonth. The figures above are an example, not your numbers.cutOffUntil(withcutOffReason) is present only while requests are refused for a used-up quota; it lifts at that moment.- This endpoint is never counted, and still answers when a quota is used up or the free plan has ended. Only a suspended account is refused.
Webhooks#
Set the webhook URL at customer.tripomatic.com/partners. You get a signing secret (whsec_...) shown once; the portal can also rotate it. A rotation takes effect at once: the old secret stops working, including for retries.
The URL must be https, on a host name (not an IP address), port 443 or 8443, with no credentials, query or fragment, and the name must resolve to public addresses. Redirects are not followed. A new webhook starts disabled; switch it on when you are ready.
What is sent#
One webhook per alert and revision, not per itinerary, as a POST of JSON. itineraries names the itineraries it concerns, each with the legs that match, as indexes into the legs as last registered (the first leg is 0), so no coordinates travel in a webhook.
{
"id": "dlv_9b1c4e7a",
"event": "created",
"createdAt": "2026-11-03T09:31:10Z",
"test": false,
"itineraries": [
{ "id": "booking-123", "legs": [0, 2] },
{ "id": "booking-456", "legs": [0] }
],
"batch": { "index": 1, "count": 1 },
"alert": {
"id": "meteoalarm-de-8c1f3a92",
"test": false,
"revision": "8f2c1a",
"status": "active",
"kind": "weather",
"subKind": "storm",
"severity": "severe",
"severityScore": 43,
"start": "2026-11-03T12:00:00Z",
"end": "2026-11-04T06:00:00Z",
"openEnded": false,
"issuedAt": "2026-11-03T09:30:00Z",
"updatedAt": "2026-11-03T09:30:00Z",
"hasGeometry": false,
"source": {
"id": "meteoalarm-de",
"name": "Meteoalarm: Germany",
"attribution": "[Deutscher Wetterdienst (DWD)](https://www.dwd.de), via [EUMETNET - MeteoAlarm](https://www.meteoalarm.org)",
"displayNotes": "Warnings issued by Deutscher Wetterdienst (DWD). Information covering more than one country: EUMETNET – MeteoAlarm. Time delays between this website and the www.meteoalarm.org website are possible. For the most up-to-date awareness information as published by the participating National Meteorological and Hydrological Services, please refer to www.meteoalarm.org."
},
"text": {
"language": "de",
"title": "Amtliche Warnung vor Sturm",
"description": "Es treten Sturmböen mit Geschwindigkeiten um 80 km/h auf.",
"areaText": "Bayern",
"sourceUrl": "https://example.org/warnings/8c1f3a92"
},
"area": {
"generalized": false,
"parts": [
{ "type": "place", "code": "DE-BY", "codeSystem": "ISO3166-2", "name": "Bayern" }
]
}
}
}The alert is in the alert format, without match and without a polygon (bbox and hasGeometry only). The headers are Content-Type: application/json, Tripomatic-Signature and Tripomatic-Event-Id (the same as id).
event | When |
|---|---|
created | The first delivery of this alert to you, for any of your itineraries. |
updated | A non-cosmetic revision of an alert already delivered, or more of your itineraries now match it. An updated with an empty itineraries list means the new revision no longer concerns any of your itineraries; treat the alert as withdrawn for you. |
ended | The source cancelled the alert, its stated end passed, or, for a source that states no end, we stopped tracking it after a period that varies by source. The alert in the body may still say status: active. |
- Only alerts reach webhooks: advisories and the calendar do not.
- The
alertis read when the webhook is sent. It is the current state, so itsrevisioncan be newer than the event that caused the webhook, and a retry can carry a newer body under the sameid. If we can no longer give you the alert (it is gone, or its source was withdrawn), it is reduced to the form below. The itineraries are still the news; useGET /v1/itineraries/{id}/alertsfor the current state.
{
"id": "meteoalarm-de-8c1f3a92",
"test": false,
"revision": "8f2c1a",
"status": "unavailable"
}itinerariesholds at most 500 itineraries. A longer list is split into batches, each its own webhook with its ownid;batch.indexcounts from 1 andbatch.countis the total. A retry sends only the batches you have not acknowledged.- The webhooks of one alert may arrive in any order. Use
alert.updatedAtandalert.revisionto know which body is newer.
From a webhook to a summary#
To show a traveller what an alert means for one stop of a trip:
- Take an entry of
itinerariesand each index in itslegs, and look up that leg as registered (GET /v1/itineraries/{id}, or your own copy). - Call
GET /v1/alerts/summarywith the leg'slat,lon,fromandto, and yourlocale. - Find the group whose
alertIdsholdsalert.id: that is the card for this alert.
The legs of an itinerary may arrive across several webhooks, when legs are found by a scan, so take the union of the legs you have seen for an itinerary and alert. The indexes refer to the legs as last registered: after you PUT an itinerary again, indexes you received earlier may no longer point at the same leg. GET /v1/itineraries/{id}/alerts gives the legs of each match for the legs as they are now.
Delivery, retries and deduplication#
- Acknowledge with any 2xx within 10 seconds. The body of your answer is ignored. Do the real work after answering.
- Anything else, a timeout included, is retried 1, 5, 15, 30 and 60 minutes after the first attempt, then hourly up to 6 hours, and then dropped. Delivery is best effort: catch up through
GET /v1/itineraries/{id}/alerts, for example after an outage of yours. - Delivery is at least once. The same webhook can arrive more than once, always with the same
id. Deduplicate onid, and make handling idempotent. - If delivery to your endpoint keeps failing, we email the contact address of your partner account. The webhook stays on.
- Webhooks stop while your account is suspended and after your free plan has ended. Using up a quota does not stop them. Webhooks that were not sent in that time are not sent later in a burst.
Verifying the signature#
Every request has a header
Tripomatic-Signature: t=1700000000,v1=2a0722e4ac505ea617f66864cfe969f1a52b2eb51aa59f3addb27def47a81236where t is the time we signed, in Unix seconds, and v1 is the HMAC-SHA256 of the string <t>.<raw body>, in lower-case hex.
- The key is the whole secret string as shown to you,
whsec_prefix included, as UTF-8 text. It is not the part afterwhsec_, and not decoded from hex or base64. - Sign the exact bytes you received: read the raw body before parsing it, because re-serialising the JSON changes the bytes.
- Compare in constant time.
- Reject old timestamps: if
tdiffers from your clock by more than a few minutes (we suggest 5), reject the request. We sign each attempt afresh, so a retry has a newt.
Worked example#
A test vector you can check your code against. The secret here is made up for documentation and is not a real one.
secret : whsec_test
t : 1700000000
body : {"id":"dlv_1"}
signed : 1700000000.{"id":"dlv_1"}
v1 : 2a0722e4ac505ea617f66864cfe969f1a52b2eb51aa59f3addb27def47a81236import hashlib, hmac, time
def verify(secret: str, header: str, raw_body: bytes, tolerance_seconds: int = 300) -> bool:
parts = dict(item.split("=", 1) for item in header.split(","))
t = parts["t"]
if abs(time.time() - int(t)) > tolerance_seconds:
return False
expected = hmac.new(secret.encode("utf-8"), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, header, rawBody, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((item) => item.split("=", 2)));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret) // the whole string, whsec_ included
.update(`${parts.t}.`)
.update(rawBody) // a Buffer of the exact bytes received
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}With the vector above, verify("whsec_test", "t=1700000000,v1=2a07...1236", Buffer.from('{"id":"dlv_1"}'), Infinity) is true (the tolerance is lifted only because the vector's time is in the past).
The alert format#
The same object is returned by /v1/alerts, /v1/alerts/{id} and /v1/itineraries/{id}/alerts, and sits in alert in webhooks. There is no machine-written text in it; for that, use the summary.
{
"id": "meteoalarm-de-8c1f3a92",
"test": false,
"revision": "8f2c1a",
"status": "active",
"kind": "weather",
"subKind": "storm",
"severity": "severe",
"severityScore": 43,
"start": "2026-11-03T12:00:00Z",
"end": "2026-11-04T06:00:00Z",
"openEnded": false,
"issuedAt": "2026-11-03T09:30:00Z",
"updatedAt": "2026-11-03T09:30:00Z",
"hasGeometry": false,
"source": {
"id": "meteoalarm-de",
"name": "Meteoalarm: Germany",
"attribution": "[Deutscher Wetterdienst (DWD)](https://www.dwd.de), via [EUMETNET - MeteoAlarm](https://www.meteoalarm.org)",
"displayNotes": "Warnings issued by Deutscher Wetterdienst (DWD). Information covering more than one country: EUMETNET – MeteoAlarm. Time delays between this website and the www.meteoalarm.org website are possible. For the most up-to-date awareness information as published by the participating National Meteorological and Hydrological Services, please refer to www.meteoalarm.org."
},
"text": {
"language": "de",
"title": "Amtliche Warnung vor Sturm",
"description": "Es treten Sturmböen mit Geschwindigkeiten um 80 km/h auf.",
"areaText": "Bayern",
"sourceUrl": "https://example.org/warnings/8c1f3a92"
},
"area": {
"generalized": false,
"parts": [
{ "type": "place", "code": "DE-BY", "codeSystem": "ISO3166-2", "name": "Bayern" }
]
},
"match": {
"type": "exact",
"explanation": "The place lies in the alert area"
}
}| Field | Meaning |
|---|---|
id | Stable for the life of the alert. |
test | true only for synthetic alerts from POST /v1/test/alerts. Present in reads and webhooks alike. |
revision | Opaque identifier of the alert's current version. Compare for equality only; it does not order. |
status | active or cancelled. Lookups return active alerts. A webhook can carry cancelled, or unavailable in the reduced form. |
kind, subKind | What it is about: for example weather, flood, earthquake, tropical_cyclone, wildfire, health_outbreak, strike, transport_disruption. subKind is a free-form finer label. New values can appear. |
severity | How the source rates it, as one of minor, moderate, severe, extreme. Absent when the source gives no rating. |
severityScore | An integer from 0 to 100 for sorting and thresholds. It combines the kind of event, its severity, how far it has progressed, how large the area is and the source. 100 is the most serious: an extreme event of the gravest kind. 0 is a notice with no hazard. For orientation, a minor fog warning is about 2, a severe thunderstorm about 43 and a severe tornado warning about 71. It depends only on the alert's current state, so it can move when the alert is revised. The scale may be recalibrated in a later version: choose thresholds on observed data, and do not read the number as a probability. |
start, end, openEnded | When it applies. openEnded: true means the source states no end, and end is absent; it does not mean the alert lasts for ever. An alert with no stated end still drops out of lists and summaries after a period that varies by source. A known future alert has a start ahead of now. |
issuedAt, updatedAt | When the issuer published it, and when it last changed. issuedAt must be shown with the alert (attribution). |
source | Who issued it: id, name, the attribution and displayNotes. attribution is Markdown limited to text and links, to be shown as it is beside the alert. See attribution. |
text | The issuer's own words: language (the issuer's, not a translation), title, description, an areaText and a sourceUrl. |
area | Where it applies, below. |
hasGeometry | true when a polygon of the area exists. It is never in lists or webhooks: read it with GET /v1/alerts/{id}, which also has the alert's revisions. |
match | On lookups and itinerary matches: why you got this alert, and for an itinerary the legs it concerns. Below. |
area#
generalized: a place the issuer named could not be found, so the alert was widened to a whole country, listed ingeneralizedTo. You may want to say "applies to all of Portugal" instead of naming a smaller place.incomplete(when present andtrue): some of the area could not be loaded; names, codes or shapes may be missing fromparts.parts: the pieces of the area. Each has atype:place: a country, region or city, withnameand, for a country (ISO3166-1-alpha3) or first-level region (ISO3166-2), acode(ISO 3166-1 alpha-3 or ISO 3166-2). A smaller place has a name and no code.entity: an airport or flight (entityType,code).shape: an area with aname, sometimes acodeandcodeSystemand, only where we are allowed to pass them on, abbox(west, south, east, north). The GeoJSONgeometryand thegeometryOriginto credit are only inGET /v1/alerts/{id}. Do not rely on a shape being there; the codes and names always are.- A part with
exclude: trueis not affected by the alert. - Ignore part types you do not know.
match#
match.type | Meaning |
|---|---|
exact | The place lies in the alert area. |
entity | The alert names this airport or flight. |
partial | The alert area partly covers the place. |
nearby | The place lies close to the alert area. |
country | The alert applies to the country of the place. |
explanation is a sentence for people; do not parse it. Other type values can appear. Webhooks carry no match, since the alert is not about one place; their itineraries name the matched legs instead.
Showing alerts: attribution#
Alerts are official publications, and their issuers ask to be credited. Wherever you show an alert's text, title or area to a person, you must show:
- the source name (
source.name), withsource.attributionbeside the alert. It is Markdown limited to text and links: show it as it is, with its links, and do not rewrite it; - the time the alert was issued (
issuedAt), in the reader's time zone or in UTC; - the
source.displayNotestext, whenever the field is present.
Also: credit geometryOrigin where you draw a shape; do not present your own rewording or translation as the issuer's text; and keep the sources of a group visible next to a machine-written summary. A group's sources carry the same credits (name, attribution, issuedAt, displayNotes). These credits come with the data, so read them from the response rather than hard-coding them.
source.displayNotes#
displayNotes is a short text for your travellers to read, in English. It belongs to the source, not to the single alert: every alert of a source carries the same text. It is not an instruction to you, and it does not describe the event. Not every source has one, so the field can be absent.
| What it holds | Example |
|---|---|
| A notice the issuer requires | Meteoalarm's disclaimer, shown in the alert format. |
| A statement that the issuer does not endorse your product | "Not an endorsement by NOAA or the National Weather Service." |
| Help with reading the alert | "Modelled impact estimates for cyclones, floods, wildfires and volcanoes; the colour says how likely international humanitarian help is needed, not how dangerous the event is for you." (the start of the GDACS note) |
What to do with it:
- Read it from each alert. Do not copy it into your code: the text can change, and it differs between sources.
- Show it with the alert, wherever you show the alert's text, title or area. Small print under the alert is fine.
- Show it word for word, in English, whatever the language of your interface: do not translate, shorten or rephrase it.
- If the field is absent, there is nothing to add.
All Meteoalarm alerts (source ids starting with meteoalarm-) carry a mandatory disclaimer here. They must not be shown without the source name, the time of issue and that disclaimer.
Quote the requestId of an error, or the X-Request-Id of a response, when you contact us about it.