HelloSafe Atlas · Travel Insurance API

Documentation

One API, one key: price a trip across insurers, read what each contract covers, mint a tracked sale link, and diagnose what a traveller's card is missing. Free sandbox, no form to fill.

Part one

Get started

From nothing to priced offers in one afternoon. Every snippet below runs as it is once you drop your own key in. One key opens every endpoint on this page.

Step 0

What you need

ItemValue
Base URLhttps://atlas.hellosafe.com
Key idak_test_... in sandbox, ak_live_... in production. One key, every endpoint: pricing, links and Coach.
Signing secretShown once, when you create the key. Server side only.
RuntimeAnything that can compute an HMAC-SHA256. Every language can.

This is a server to server API. There is no CORS header on any response, on purpose: a signing secret that reaches a browser is a secret you have to revoke.


Step 1

Get your sandbox key

Create a partner account, open API in the dashboard, and click Create my test key. You get a key id and a signing secret on the spot. Nobody reviews anything, because a sandbox key is inert: it answers from fixed sample data, calls no insurer, creates no real quote and credits no sale.

The secret is shown once. We store it in a way that lets us verify your signatures, and we cannot display it again. Put it in your secret manager on the spot. If you lose it, roll it: same key id, new secret.

Sandbox quota: 200 prices and 50 links per day, and 60 calls per minute whatever the endpoint. The daily figure is what an integration day needs; the per minute ceiling is what stops a runaway loop.


Step 2

Sign a request

Three headers on every call:

HeaderValue
x-atlas-key-idYour key id.
x-atlas-timestampNow, in unix seconds. Rejected beyond 5 minutes off (replay guard), so keep your clock synced.
x-atlas-signaturev2= followed by the hex HMAC-SHA256 of the message below, keyed with your signing secret.
The signed messagev2 scheme
`${ts}.${METHOD}.${pathname}.${rawBody}`

e.g.  1787654321.POST./api/v1/travel/quotes.{"trip":{...},"language":"en"}
Sign the exact bytes you send. The signature covers the raw body, character for character. Build the JSON string once, sign that string, and post that same string. Re-serialising between signing and sending is the one mistake everybody makes, and it looks like a broken secret.

Step 3

Your first priced quote

One call describes one trip: dates, destinations, ages. You get the offers that cover it, sorted cheapest first, each with its price and the detail of every guarantee.

Node.js

node >= 18 (built in fetch and crypto)quote.mjs
import { createHmac } from "node:crypto";

const KEY_ID = process.env.ATLAS_KEY_ID;        // ak_test_... (sandbox)
const SECRET = process.env.ATLAS_SIGNING_SECRET; // shown once, at creation

const body = JSON.stringify({
  trip: {
    intent: "forTourism",
    startDate: "2026-09-10",
    endDate: "2026-09-24",
    countryResidence: "FR",
    arrivalCountries: ["TH"],
    travellers: [{ age: 32 }]
  },
  language: "en"
});

const ts = Math.floor(Date.now() / 1000).toString();
const sig = "v2=" + createHmac("sha256", SECRET)
  .update(`${ts}.POST./api/v1/travel/quotes.${body}`)
  .digest("hex");

const res = await fetch("https://atlas.hellosafe.com/api/v1/travel/quotes", {
  method: "POST",
  headers: {
    "content-type": "application/json",
    "x-atlas-key-id": KEY_ID,
    "x-atlas-timestamp": ts,
    "x-atlas-signature": sig
  },
  body
});

const { sessionId, offers } = await res.json();
console.log(offers.length, "offers, cheapest:", offers[0].price.amount);
// Keep sessionId next to what you display: it is THIS traveller's quoting
// session, and /links will require it.

cURL

bash + opensslrun in a terminal
KEY_ID="ak_test_..."
SECRET="sk_test_..."
BODY='{"trip":{"intent":"forTourism","startDate":"2026-09-10","endDate":"2026-09-24","countryResidence":"FR","arrivalCountries":["TH"],"travellers":[{"age":32}]},"language":"en"}'
TS=$(date +%s)
SIG="v2=$(printf '%s.POST./api/v1/travel/quotes.%s' "$TS" "$BODY" \
  | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)"

curl -s https://atlas.hellosafe.com/api/v1/travel/quotes \
  -H "content-type: application/json" \
  -H "x-atlas-key-id: $KEY_ID" \
  -H "x-atlas-timestamp: $TS" \
  -H "x-atlas-signature: $SIG" \
  -d "$BODY"

PHP

php >= 8quote.php
<?php
$keyId  = getenv('ATLAS_KEY_ID');
$secret = getenv('ATLAS_SIGNING_SECRET');
$path   = '/api/v1/travel/quotes';

$body = json_encode([
  'trip' => [
    'intent'           => 'forTourism',
    'startDate'        => '2026-09-10',
    'endDate'          => '2026-09-24',
    'countryResidence' => 'FR',
    'arrivalCountries' => ['TH'],
    'travellers'       => [['age' => 32]],
  ],
  'language' => 'en',
]);

$ts  = (string) time();
$sig = 'v2=' . hash_hmac('sha256', "$ts.POST.$path.$body", $secret);

$ch = curl_init('https://atlas.hellosafe.com' . $path);
curl_setopt_array($ch, [
  CURLOPT_POST           => true,
  CURLOPT_POSTFIELDS     => $body,          // the SAME string you signed
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER     => [
    'content-type: application/json',
    "x-atlas-key-id: $keyId",
    "x-atlas-timestamp: $ts",
    "x-atlas-signature: $sig",
  ],
]);
$response = json_decode(curl_exec($ch), true);
echo count($response['offers']), " offers\n";

What comes back

200 OKapplication/json
{
  "ok": true,
  "mode": "sandbox",
  "sessionId": "qs_1f2e3d4c5b6a79880919a2b3c4d5e6f7.9a8b…",
  "offers": [
    {
      "id": 9001,
      "name": "Sandbox Essential",
      "plan": "Essential",
      "insurer": { "name": "Sandbox Assurance", "logoUrl": null },
      "price": {
        "amount": 38.40,
        "amountInCents": 3840,
        "currency": "EUR",
        "insurerAmount": 38.40,
        "insurerCurrency": "EUR",
        "isStartingPrice": false,
        "period": null
      },
      "guaranteeCurrency": "EUR",
      "guarantees": {
        "hospitalFeesAbroad": { "state": "value", "value": 100000 },
        "rapatriationAssistance": { "state": "actual_costs" },
        "suitcaseInsurance": { "state": "value", "value": 1000 },
        "cancelTrip": { "state": "not_available" }
      },
      "highlights": { "included": ["..."], "excluded": ["..."] },
      "documents": { "cgvUrl": null, "ipidUrl": null },
      "position": 1
    }
  ],
  "unpricedCount": 0,
  "redirectOnlyCount": 0,
  "trip": { "...": "the trip we priced, normalised" },
  "quote": {
    "mode": "sandbox",
    "days": 15,
    "travellers": 1,
    "expiresAt": "2026-09-01T12:30:00.000Z"
  },
  "meta": { "apiVersion": "1.0.0", "language": "en", "notice": "Sandbox data..." }
}

Full field by field description in the reference below. The live response has the same shape, with real insurers and real premiums.

insurer.logoUrl is the offer's pictogram now, not the insurer's logo. This changed in 2.0.0. Our own comparator stopped showing insurer logos on brokerage offers: each offer carries its own icon, a pictogram on a tile. The API follows, and it follows in the field you are already reading, so your page shows the new image with no change on your side. The name is a misnomer and we kept it deliberately, so that the switch reaches you without work. The image is an SVG served by the very function that draws it on hellosafe.com, so what you display is what our own visitors see. Add ?size=16..512 if your layout wants another size: it is vector, so any size stays sharp.

The insurer is still named. insurer.name is untouched, so "underwritten by Chapka" keeps working, which is what your customer needs to know. What is no longer published, for an offer that has an icon, is that insurer's logo.

iconUrl tells you which of the two you got. It carries the same URL as logoUrl when the offer has an icon, and is absent, never null and never an empty string, when it has none, in which case logoUrl fell back to the insurer's logo. Reading logoUrl alone can no longer tell those apart. Every offer we serve carries an icon today, so the fallback is not hit in practice, but the sandbox's cheapest plan deliberately has none, which is why you cannot see the field in the response above: code that path before you go live rather than after.

redirectOnlyCount is the offers we deliberately do not return. The catalogue also carries offers sold by sending the traveller to the insurer's own site. Those cannot be attributed to you: the sale leaves HelloSafe, no subscription exists, the conversion postback never fires and you would not be paid. They are also a dead end for the traveller, who would have to key the whole trip in again. The count is normally 0: they are excluded before we ever see them. A non-zero value means one slipped past that exclusion and our own filter caught it. Every offer you DO get back is one you can be paid on.



Try it

Run every endpoint, right here

Paste a sandbox key and send a real signed request to each endpoint. The request is signed in your browser and answered by the live sandbox - no install, no server, nothing billed or credited. Each endpoint carries its schema, a Node snippet and the exact curl, so you can copy what worked.

Prefer your own client? Download the Postman collection: set your sandbox key once and every request signs itself, and the Quotes request saves its sessionId so Links works on the next click. In the playground below, run Quotes first and paste its sessionId into the Links body.

Paste a sandbox key to try every endpoint below. Don't have one? Create one free - one click, no review. It's held in memory only: never stored, never sent - only the signature is.
GET/api/v1/travel/metaneeds your key

Reference data and key state

full reference ↓
POST/api/v1/travel/quotesneeds your key

Price a trip

full reference ↓
POST/api/v1/travel/linksneeds your key

Mint a tracked subscription link

full reference ↓
POST/api/v1/coach/bilanneeds your key

Coach bilan: coverage gaps and ranked sell arguments

full reference ↓
GET/api/v1/coach/cardsneeds your key

Card catalogue: the banks and cards the Coach knows

full reference ↓

Step 5

Go live

Live access is a short review. It is not a sales threshold: we size your daily quota against the traffic you expect, and we record who presents the guarantees to the traveller, which is the question a regulator would ask us.

Before you applyWhy
10 sandbox prices calledProves the signature and the trip payload work.
1 sandbox link mintedProves the whole funnel handoff works, not just the read.
Partner account approvedA live key credits real sales, so there has to be someone to credit.

The checklist turns green on its own in your dashboard, and the application asks for your expected volume, your markets and your distribution status. Approval sets your quotas; you then create the live key yourself, so the signing secret is never seen by anyone at HelloSafe.

Going live gives you a second key, not a promoted one. Your sandbox key stays alive with its own quota, so your tests keep running against fixtures after production traffic starts. The only thing that changes in your code is the pair of credentials it reads from the environment.


Good to know

The five things that go wrong

SymptomCause
INVALID_SIGNATUREThe body was re-serialised after signing, or the pathname in the signed message does not match the URL you called.
MISSING_SESSION_ID/links called without the sessionId from the /quotes response. Sessions are issued, never invented: carry it from quote to click.
STALE_TIMESTAMPServer clock drift. Sync it with NTP.
CANCELLATION_NEEDS_TRIP_PRICEshouldCoverCancellation is true without a tripPrice to insure.
QUOTA_EXCEEDEDDaily bucket spent. Read X-RateLimit-Remaining on every response and pace yourself instead of discovering the ceiling.

Every error code, with its HTTP status, is listed in the error table. The machine readable contract is openapi.json.


Part two

Endpoints

Generated from the spec, version 2.3.0, so what you read here is what the API answers.

Quotes

Price a trip and read the catalogue vocabulary. This is what most integrations use.

GET /api/v1/travel/meta

Reference data and key state

The vocabulary a caller would otherwise hard-code: the 15 trip types, the 26 funnel languages, the 51 US state codes a US resident's stateResidence accepts, the guarantee slugs with their English labels and groups, the guarantee states a response can carry, the request ceilings, and your key's own environment and quota. Does not consume quota.

Responses

CodeMeaning
200 Reference data.
401
403
POST /api/v1/travel/quotes

Price a trip

Prices one trip against the travel catalogue and returns the priced offers, cheapest first, each with its premium, its guarantee ceilings and its policy documents. Read-only: nothing is stored, no subscription is created and no attribution happens here.

Every response also opens a quoting session: `sessionId` is a signed token identifying THIS traveller's flow, and POST /links requires it. To keep one traveller's session across a trip edit, echo the previous `sessionId` in the body — a valid one is returned unchanged, anything else silently starts a fresh session (a broken continuation never fails a pricing call). Never share one sessionId across travellers.

Body

FieldTypeReqDescription
trip object yes
sessionId string no Optional: the sessionId from THIS traveller's previous /quotes response, to keep their session across a trip edit. A valid token is echoed back; anything else silently starts a fresh session. Never reuse one across travellers.
language bg · cs · da · de · el · en · es · et · fi · fr · hr · hu · is · it · lt · lv · mt · nl · no · pl · pt · ro · sk · sl · sv · tr no Funnel language.

Responses

CodeMeaning
200 Priced offers.
400
401
403
429 Refused. QUOTA_EXCEEDED when the daily bucket is spent (Retry-After points at the next UTC day), RATE_LIMITED when more than 60 calls left in the current minute (Retry-After points at the next minute). Neither refusal is counted against your quota.
502 Upstream pricing failed.
504 Upstream pricing timed out.

Coach

What a traveller's bank card does not cover, and the arguments that sell the gap.

POST /api/v1/coach/bilan

Coach bilan: coverage gaps and ranked sell arguments

Analyses a traveller's situation against the travel cover carried by their bank card, for the chosen destinations, and returns it as data: the card's GAPS (guarantees in default, adequately covered ones are omitted), a ranked list of arguments each with a strength and a category, the medical recommendation, a social-proof figure and an attributed quote link.

Sell-only by design: it returns the card's shortcomings, never a reason not to buy, except bilan.medical, which gives the trip's recommended medical ceiling (it follows the destination) and whether the card meets it. Copy comes back as message KEYS plus interpolation variables, not finished sentences, so you render it in your own wording. Requires a key carrying the `coach` scope, which every self-serve key now has: the sandbox answers from the real engine, and its `offer.quoteUrl` comes back null because a sandbox key credits nobody.

Body

FieldTypeReqDescription
residence string yes ISO 3166-1 alpha-2. Drives the health socle and the default market.
destinations array of string yes Where they travel. ISO 3166-1 alpha-2, 1 to 50 codes.
card object no Exactly one card mode: { catalogueId } | { bin } | { bank, network, tier } | { network, tier } | { none: true }. catalogueId names the exact card: take it from GET /api/v1/coach/cards (a sandbox key identifies cards by BIN or bank instead, see key.cardModes there). A BIN or { bank, network, tier } match by range: at a bank with two cards of one tier, the most basic one wins. A card that cannot be resolved never fails the call: it degrades to a baseline card and adds a code to meta.warnings.
trip object no Optional booleans, all default false.
market fr · us · ca · sg · my · universal no Currency, formatting and health socle. Derived from residence when omitted.
ref string no Overrides the attribution ref baked into the returned quote URL.

Responses

CodeMeaning
200 The bilan. See the guide for the full field by field description.
400 INVALID_JSON, BAD_RESIDENCE or NO_DESTINATIONS.
401 UNAUTHORIZED, STALE_TIMESTAMP or INVALID_SIGNATURE.
403 SCOPE_FORBIDDEN: the key lacks the coach scope.
502 AUTH_LOOKUP_FAILED or CARD_RESOLVE_FAILED.
GET /api/v1/coach/cards

Card catalogue: the banks and cards the Coach knows

The bank cards the Coach knows, per country of residence: each bank, then its cards, with the name printed on the card, its picture, the tier the Coach reads from that name, and the catalogueId that names the card exactly in POST /api/v1/coach/bilan. Built for a card picker: the traveller's bank, then their card, then the bilan with { catalogueId }. byBankTier says whether { bank, network, tier } would land on that same card: at a bank with two cards of one tier, only the most basic one is reachable that way. Travellers whose bank is not listed fall back to the generic { network, tier } profiles listed under generic. Signed like every endpoint, with an empty body: the query string is not signed. Does not consume quota.

Query parameters

ParameterReqDescription
country no Country of residence, ISO 3166-1 alpha-2. Omitted: every country with a bank list (FR, US, CA, SG, MY). Any other valid code returns an empty banks list.
lang no Language of the card names. Default: French for FR, English elsewhere.

Responses

CodeMeaning
200 The catalogue.
400
401
403

Conversion

Report a sale you took yourself, so it can be commissioned.

POST /api/postback/conversion

Report a conversion

Signed server-to-server postback that reports a sale against a tracked link. The short code is re-resolved to a real tracked link and the partner code is checked before any commission is recorded.

Body

FieldTypeReqDescription
ref string yes The tracked ref that carried the sale, partnerCode-shortCode.
externalOrderId string yes Your own order id. Replaying it updates that sale instead of creating a second one.
amount number yes Premium paid by the traveller, in the sale currency.
commission number no Optional. Left out, it is computed server side from the configured rate.
currency string no ISO 4217. Defaults to EUR.
status pending · validated · cancelled yes State of the sale. A cancelled sale reverses the commission.
partnerFeeAmount number no Optional. The uplift slice of the handling fee you actually charged on this sale, in the sale currency. Declarative: the ledger recomputes what it should be from the partner's own grid and pays the lower of the two, so this can only ever reduce what is owed, never inflate it. Leave it out when the offer carried no uplift.
contractVersion 3.1 · 3.2 no Optional. Which version of this contract you are speaking. Omitting it means 3.1, the shape that predates partnerFeeAmount, and stays valid. A version this endpoint does not know is refused with 400 UNKNOWN_CONTRACT_VERSION rather than parsed leniently: a field silently dropped here is a sale booked at the wrong price, and past the 14-day auto-validation that is no longer repairable.

Responses

CodeMeaning
200 Conversion accepted.
401

Environments

Sandbox and live

The URL never changes. The key decides.

Sandbox ak_test_Live ak_live_
PricesFixed sample data, fictional insurerThe real catalogue, rated by each insurer
Insurer callsNoneOne per insurer, per request
Quote created by /linksNoneA real quoting-stage subscription
AttributionNone (ref: null)Your affiliate ref, on the subscription
Grantedself-serve one clickShort review
Default quota200 reads, 50 writes per day, 60 per minuteSet from your declared volume

The response shape is identical in both, deliberately: an integration built against the sandbox runs unchanged in production. Start at the quickstart above.


Authentication

Signed requests

HeaderValue
x-atlas-key-idYour key id.
x-atlas-timestampUnix seconds. Rejected beyond a 5 minute window.
x-atlas-signatureScheme prefix, then hex HMAC-SHA256 keyed with your signing secret.

Signature scheme

SchemeSigned messageUse
v2= Covers the timestamp, the method, the path and the raw body. required
v1= Covered the timestamp and the raw body only. Retired. Every key refuses it with V1_SCHEME_RETIRED.
The signed messagehex HMAC-SHA256, keyed with your secret
v2   `${ts}.${METHOD}.${pathname}.${rawBody}`

# GET has an empty body, so the message simply ends on the dot:
v2   `${ts}.GET./api/v1/travel/meta.`

v2 binds the signature to the endpoint it was minted for. Under v1 a captured signature replayed on any other route of the same API, since only the body was covered. That is why v1 is retired: a v1 signature answers 401 V1_SCHEME_RETIRED.

Rotating your secret

Rotating issues a new signing secret and keeps the previous one working for 24 more hours, so you can deploy through a normal release instead of scheduling downtime. The dashboard shows the deadline until the window closes. Revoking is different: it stops the key, and the old secret with it, immediately. That is the path to use if a secret has leaked.


Shared payload

The trip object

Used by /quotes and /links alike. tripInfo is accepted as an alias for trip.

FieldReqDescription
intentyesOne of the 16 trip types below.
startDateyesYYYY-MM-DD.
endDateyesYYYY-MM-DD, on or after startDate.
countryResidenceyesISO 3166-1 alpha-2. Drives eligibility and currency.
stateResidencenoUS residents only: the state they live in, as its USPS code (NY, TX), one of the 50 states or DC. US travel insurance is regulated state by state, so send it whenever you have it. It changes neither offers nor prices, and it is ignored for any other residence. The territories are countries, not states: a Puerto Rico resident is countryResidence PR.
arrivalCountriesyesArray of alpha-2 codes, 1 to 20.
travellersyesArray of objects carrying an age, 1 to 50. Above roughly 10 the individual products give way to the group product.
tripPricecond.Insured trip cost: the total price of the trip for the whole party, not a per-traveller amount. Required when shouldCoverCancellation is true.
currencynoISO 4217. The currency we price in, and the one tripPrice and studiesAmount are read in. Default EUR. 33 codes convert; anything else leaves prices in the insurer currency instead of failing the call.
shouldCoverCancellationnoDefault false.
shouldCoverExtremeSportsnoDefault false.
isAnnualnoForced true by the annual and expat intents.
language (outside trip)noFunnel language, default en. 26 supported, see GET /meta.

The ceilings are deliberate: one call describes one trip, not a customer file.

Trip types

intentCovers
forTourismLeisure trip, the default
schengenAreaSchengen visa application, meets the 30k EUR requirement
annualMulti-trip annual cover (forces isAnnual)
studyInternshipStudies or internship abroad
whvWorking holiday visa
cruiseCruise
digitalNomadLong remote-work stay
expatExpatriation (forces isAnnual)
groupTravelGroup travel
rentalStayRental accommodation stay
mountainTripSki and mountain trip
backToHomeReturn to the home country
humanitarianHumanitarian mission
auPairAu pair stay
toWorkWork assignment abroad
cancellationCancellation cover only

Vocabulary

Prices and currency

price.amount is what the traveller pays. Send a currency in the trip and we convert with the same rate, the same conversion margin and the same rounding the subscription funnel displays and the card is charged, so the figure you show and the figure we debit are the same one.

price.insurerAmount and price.insurerCurrency are the insurer's own price, before conversion. They are there for reconciliation. Converting them yourself is the one mistake to avoid: your rate is not ours, so your customer is quoted less than they are charged and finds out at the payment screen. When no conversion happened the two amounts are equal, so displaying amount is always correct.

Currency codes are uppercase on every field.

Your own rate

A partner can set their own commission from their dashboard, between the base rate their account carries and 50%. Everything above the base rate is funded by raising the handling fee on the policy, so the traveller pays the difference, and it applies on every channel: tracked links, the widget, and this API.

price.amount already includes it. It is still what the traveller pays and still what the card is charged, so nothing about the rule above changes. When you charge an uplift, the response also carries meta.partnerFee with feeBps (the whole handling fee inside those prices), upliftBps (your share of it) and commissionBps (what a sale pays you), in basis points of the insurer premium. It is a breakdown, never an amount to add on top. Without an uplift the object is simply absent.

The rate is frozen onto each sale when the conversion is booked, so moving it never rewrites what an earlier sale paid.


Vocabulary

Guarantees

offer.guarantees maps a slug to a state and, when the state carries a ceiling, a value. Ceilings are expressed in guaranteeCurrency, which is not always the price currency.

stateRender it as
valueCovered up to value, in guaranteeCurrency.
includedCovered, no published ceiling.
actual_costsCovered at actual cost, no ceiling.
per_dayCovered up to value per day.
return_ticketCovered in kind: a return ticket home.
trip_priceCovered up to the insured trip cost (value echoes it).
studies_amountCovered up to the insured tuition amount.
not_availableNot covered by this offer.

Slugs

healthLabel
hospitalFeesAbroadHospital fees abroad
directBillingDirect billing
hospitalFeesHomeHospital fees at home
dailyAllowanceDaily allowance
disabilityInsuranceDisability or death
assistanceLabel
rapatriationAssistanceRepatriation assistance
earlyReturnEarly return
researchFeesSearch and rescue fees
cashAdvanceCash advance
tripLabel
cancelTripTrip cancellation
stayInterruptionStay interruption
studiesInterruptionStudies interruption
transportDelayTransport delay
belongingsLabel
suitcaseInsuranceSuitcase insurance
electronicsInsuranceElectronics insurance
liabilityLabel
privateRcPrivate liability
rentalRcRental liability
activitiesLabel
skiInsuranceSki and mountain insurance
extremeSportsExtreme sports
expatLabel
expatHospitalFeesAccidents and hospitalisation
expatMedicalFeesMedical expenses
expatRepatriationRepatriation assistance
expatDentalFeesDental fees
expatOpticalFeesOptical fees
expatCheckupFeesVaccination and checkups
expatCivilLiabilityCivil liability
expatMaternityMaternity
expatTelemedicineTeleconsultation

Labels are English. Localise them on your side, or read the live list from GET /api/v1/travel/meta, which is the authority as the catalogue grows. Treat an unknown slug as a forward compatible addition rather than an error.


No key needed

Embed, without writing a line of code

Not every integration needs an API. The Marketplace embed puts the whole HelloSafe comparator on your page with one script tag: the visitor compares, subscribes and pays without leaving you, and the sale is credited to your account.

Paste where the comparator should appearhtml
<div id="hellosafe-widget"></div>
<script src="https://atlas.hellosafe.com/widget.js"
        data-ref="YOUR-TRACKING-CODE"
        data-intent="forTourism"
        data-residence="FR"
        data-lang="en"
        data-height="900"></script>

Your own snippet, with your tracking code and your defaults already filled in, is generated in the dashboard under Widget. It carries the trip type, the residence country and the language you choose, and each configuration gets its own tracked link so your reporting stays split by page or campaign.


Limits

Quotas

Two ceilings, and they do different jobs. The daily buckets bound what a key does over a day: reads are the pricing and Coach calls, writes are the minted links, capped much lower because one link is one traveller. The burst ceiling bounds one minute, all endpoints together, and it is what actually stops a runaway loop: a daily cap only notices once the damage is done.

On every responseheaders
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 173
X-RateLimit-Reset: 41230           # seconds until the daily bucket rolls
X-RateLimit-Burst-Remaining: 57    # calls left in the current minute

Over the daily cap you get 429 with QUOTA_EXCEEDED, the bucket name and Retry-After. Over the minute you get 429 with RATE_LIMITED and a Retry-After of a few seconds. Neither refused call is counted against you, and being throttled never costs you a day: on a burst refusal the daily headers keep describing the day, so X-RateLimit-Remaining is what is still yours once the minute rolls, not zero.

Reading the headers beats discovering the ceiling: they are on every answer, including the Coach.


Edge cases

Errors

The body is a JSON object with an error field carrying the code below, sometimes with a detail field beside it.

HTTPCodeCause
400 INVALID_JSON Body is not valid JSON.
400 BAD_LANGUAGE Language outside the 26 the funnel serves.
400 BAD_TRIP trip is missing or not an object.
400 BAD_INTENT intent is not one of the 16 trip types.
400 BAD_DATES Dates are not YYYY-MM-DD, or endDate is before startDate.
400 BAD_RESIDENCE countryResidence is not a 2 letter code.
400 BAD_STATE_RESIDENCE countryResidence is US and stateResidence is not one of the 50 states or DC. A territory such as PR is a countryResidence.
400 NO_DESTINATIONS arrivalCountries is empty.
400 TOO_MANY_DESTINATIONS More than 20 arrival countries.
400 NO_TRAVELLERS travellers is empty.
400 TOO_MANY_TRAVELLERS More than 50 travellers.
400 CANCELLATION_NEEDS_TRIP_PRICE Cancellation cover without a tripPrice.
400 BAD_LINK_CODE linkCode is not alphanumeric, 4 to 32 characters.
400 MISSING_SESSION_ID /links without a sessionId. Take it from the /quotes response.
400 BAD_SESSION_ID The sessionId was not issued by /quotes (or was truncated).
400 BAD_OFFER_ID offerId is not a positive integer offer id from /quotes.
401 UNAUTHORIZED Missing, unknown or revoked key.
401 V1_SCHEME_RETIRED The signature uses the retired v1 scheme. Sign the v2 message.
401 STALE_TIMESTAMP Timestamp outside the 5 minute window.
401 INVALID_SIGNATURE Signature does not verify against the raw body.
403 SCOPE_FORBIDDEN Key lacks the quotes or quotes.link scope.
403 NO_ATTRIBUTION_CONFIGURED Live key with no partner to credit.
404 LINK_NOT_FOUND linkCode is unknown, archived, or not yours.
429 QUOTA_EXCEEDED Daily bucket spent. Retry-After says when it resets.
502 PRICING_UNAVAILABLE Upstream pricing refused the call.
502 QUOTE_CREATE_FAILED Upstream refused to create the quote.
504 PRICING_UNAVAILABLE Upstream pricing timed out.
504 QUOTE_CREATE_UNAVAILABLE Upstream quote creation timed out.

Good to know

Versioning and terms

The path is versioned. Structured fields are stable; new guarantee slugs, trip types and offers can appear, so treat unknown values as forward compatible additions. What an integration accepts: no reselling or redistribution of the prices, no long lived caching of a premium, insurer name and documents stay attached to the offer you display, no real personal data in the sandbox, and server to server only. A secret in a browser bundle is a revoked key.

Ready to build? Back to the quickstart · Get a free sandbox key · openapi.json