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.

Base URL
https://api.tripomatic.com

The 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 match tells 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#

  1. 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.
  2. 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"
  3. 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 are YYYY-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-Id header, and every error body repeats it as requestId. 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"
  }
}
StatusCodeMeaning
400invalid_requestA parameter or the body is malformed or missing. The message never repeats what you sent.
401invalid_keyThe key is missing, unknown, revoked or expired.
403partner_suspendedYour account is suspended.
403free_plan_expiredYour free plan has ended.
404not_foundNo such alert, itinerary, country or region for you, or no such route. An alert we cannot pass on to partners is also not_found.
405method_not_allowedThe route does not take this method.
409webhook_not_configuredYou asked for a test webhook, and no webhook is set up.
429rate_limitedToo many calls in a short window: a rate limit. Has Retry-After. The call does not count against your quota.
429quota_exceededA monthly quota is used up. Has Retry-After, the seconds until it resets on the 1st at 00:00 UTC.
500internal_errorA bug on our side. Quote the requestId.
503upstream_unavailableA 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_exceeded with Retry-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_exceeded as 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/usage and GET /v1/locales never 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#

EndpointPurpose
GET /v1/alertsAlerts in force at a point, in a country or in a region
GET /v1/alerts/summaryThe 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/advisoriesGovernment travel advisories of a country
GET /v1/localesThe values locale takes
GET /v1/calendarHolidays, observances and events
GET /v1/itinerariesList 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}/alertsAlerts that match an itinerary now
POST /v1/test/alertsSend a test webhook to yourself
GET /v1/usageYour daily usage and quotas
GET /docs, GET /docs/openapi.yamlThis 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, lonA 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.
isoInstead 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, cursorPaging.

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 (from and to), 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, lonA 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.
isoA country (ISO 3166-1 alpha-2 or alpha-3) or a region (ISO 3166-2), as for /v1/alerts.
localeRequired. 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.
timeZoneOptional, 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, toBoth 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 alertIds lists every alert it covers, and sources the issuers. The text is machine-written: say so where you show it, and show next to it each source's name, attribution, issuedAt (the latest among that source's alerts in the group) and displayNotes when present (attribution). A group has no end if any member alert is open-ended. The cards are in display and notification; 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. When true, the search stopped early and an alert may be missing, as for lists.
  • revision of 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, truncated false) carries Cache-Control: private, max-age=3600, so you may keep it for an hour; any other answer is no-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-8c1f3a92

GET/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, levelNative and sourceUrl. Show the issuer's level and source, not only the merged text. source.attribution is Markdown, as for alerts.
  • origin and statementOrigin say how a text was made: machine (written by a language model from the issuers' words) or template (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 the template with "pending": true: ask again later.
  • A country with no advisory answers 200 with "summary": null and "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 from to its last to: enough to go around the world in 80, with two to spare. The last to is 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 whole PUT during 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 from to the last to, 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 a PUT is 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 as updated if 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.
  • PUT replaces the whole itinerary and is safe to repeat; changed is false when the legs were already as sent.
  • Legs. 1 to 100 legs. lat and lon are decimal degrees; we do not need a precise position, so to avoid passing on a user's exact location, round to two decimal places. from is before to (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_exceeded and nothing is stored; a PUT that adds no new day is never refused.
  • If storing fails the answer is 503: send the same PUT again. 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 in requests. webhooks: webhooks your endpoint acknowledged, test alerts excluded.
  • quota holds your monthly quotas: requestsPerMonth and itineraryDaysPerMonth. The figures above are an example, not your numbers.
  • cutOffUntil (with cutOffReason) 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).

eventWhen
createdThe first delivery of this alert to you, for any of your itineraries.
updatedA 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.
endedThe 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 alert is read when the webhook is sent. It is the current state, so its revision can be newer than the event that caused the webhook, and a retry can carry a newer body under the same id. 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; use GET /v1/itineraries/{id}/alerts for the current state.
{
  "id": "meteoalarm-de-8c1f3a92",
  "test": false,
  "revision": "8f2c1a",
  "status": "unavailable"
}
  • itineraries holds at most 500 itineraries. A longer list is split into batches, each its own webhook with its own id; batch.index counts from 1 and batch.count is the total. A retry sends only the batches you have not acknowledged.
  • The webhooks of one alert may arrive in any order. Use alert.updatedAt and alert.revision to 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:

  1. Take an entry of itineraries and each index in its legs, and look up that leg as registered (GET /v1/itineraries/{id}, or your own copy).
  2. Call GET /v1/alerts/summary with the leg's lat, lon, from and to, and your locale.
  3. Find the group whose alertIds holds alert.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 on id, 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=2a0722e4ac505ea617f66864cfe969f1a52b2eb51aa59f3addb27def47a81236

where 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 after whsec_, 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 t differs 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 new t.

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     : 2a0722e4ac505ea617f66864cfe969f1a52b2eb51aa59f3addb27def47a81236
import 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"
  }
}
FieldMeaning
idStable for the life of the alert.
testtrue only for synthetic alerts from POST /v1/test/alerts. Present in reads and webhooks alike.
revisionOpaque identifier of the alert's current version. Compare for equality only; it does not order.
statusactive or cancelled. Lookups return active alerts. A webhook can carry cancelled, or unavailable in the reduced form.
kind, subKindWhat 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.
severityHow the source rates it, as one of minor, moderate, severe, extreme. Absent when the source gives no rating.
severityScoreAn 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, openEndedWhen 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, updatedAtWhen the issuer published it, and when it last changed. issuedAt must be shown with the alert (attribution).
sourceWho 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.
textThe issuer's own words: language (the issuer's, not a translation), title, description, an areaText and a sourceUrl.
areaWhere it applies, below.
hasGeometrytrue 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.
matchOn 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 in generalizedTo. You may want to say "applies to all of Portugal" instead of naming a smaller place.
  • incomplete (when present and true): some of the area could not be loaded; names, codes or shapes may be missing from parts.
  • parts: the pieces of the area. Each has a type:
    • place: a country, region or city, with name and, for a country (ISO3166-1-alpha3) or first-level region (ISO3166-2), a code (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 a name, sometimes a code and codeSystem and, only where we are allowed to pass them on, a bbox (west, south, east, north). The GeoJSON geometry and the geometryOrigin to credit are only in GET /v1/alerts/{id}. Do not rely on a shape being there; the codes and names always are.
    • A part with exclude: true is not affected by the alert.
    • Ignore part types you do not know.

match#

match.typeMeaning
exactThe place lies in the alert area.
entityThe alert names this airport or flight.
partialThe alert area partly covers the place.
nearbyThe place lies close to the alert area.
countryThe 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), with source.attribution beside 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.displayNotes text, 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 holdsExample
A notice the issuer requiresMeteoalarm'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.