Overview
What the API does
The Coach analyses a traveller's situation against the travel-insurance cover carried by their bank card, for the chosen destinations. It returns, as data: the card's coverage gaps (the guarantees in default), a ranked list of arguments (each with a strength and a category), the medical recommendation, a social-proof figure, and an attributed quote link.
bilan.medical gives the trip's recommended medical ceiling, which follows the destination, and says whether the card meets it. When it does, the headline says so and the arguments lead with the card's conditions and exclusions.
A second endpoint lists the bank cards the Coach knows, bank by bank, so you can ask the traveller for their card in a picker instead of a free-text field. See the card catalogue.
Before you start
What you need
| Item | Value |
|---|---|
| Base URL | https://atlas.hellosafe.com |
| Bilan | POST /api/v1/coach/bilan |
| Card catalogue | GET /api/v1/coach/cards |
| Key | A key id and a signing secret, from the API keys page of your dashboard. One key opens the whole API, the Coach included. |
The sandbox key takes one click and runs the real Coach engine. Its offer.quoteUrl comes back null, because a sandbox key credits nobody. This is a server to server API: call it from your backend, never from a browser, and never expose the secret client side.
Authentication
Signing a request
Every request carries three headers. The signature is an HMAC-SHA256 over the method, the path and the raw request body, so you sign exactly the bytes you send.
| Header | Value |
|---|---|
x-atlas-key-id | Your key id. |
x-atlas-timestamp | Current time in unix seconds. Rejected if more than 5 minutes off (replay guard). |
x-atlas-signature | v2= followed by the hex HMAC-SHA256 of the message below, keyed with your signing secret. |
v2 `${ts}.${METHOD}.${pathname}.${rawBody}`
# The bilan signs its JSON body. The catalogue is a GET with no body,
# so its message ends on the dot:
v2 `${ts}.POST./api/v1/coach/bilan.${rawBody}`
v2 `${ts}.GET./api/v1/coach/cards.` v2 binds the signature to the endpoint it was minted for. The query string is not part of the message. The older v1 scheme covered the body alone, so a signature captured on one route replayed on another: it is retired, and a v1 signature answers 401 V1_SCHEME_RETIRED. HMAC-SHA256 exists in every language, and the snippets below run as they are once you drop in your credentials.
Quickstart
Make your first call
Node.js
import crypto from "node:crypto";
const KEY_ID = "<YOUR_KEY_ID>";
const SIGNING_SECRET = "<YOUR_SIGNING_SECRET>";
const ENDPOINT = "https://atlas.hellosafe.com/api/v1/coach/bilan";
const payload = {
residence: "FR", // traveller residence (ISO alpha-2)
destinations: ["US"], // where they travel
card: { network: "visa", tier: "premium" },
trip: { friends: true, longTrip: true, riskyActivity: true },
market: "fr",
};
const body = JSON.stringify(payload);
const ts = Math.floor(Date.now() / 1000).toString();
const signature =
"v2=" + crypto.createHmac("sha256", SIGNING_SECRET)
.update(`${ts}.POST./api/v1/coach/bilan.${body}`).digest("hex");
const res = await fetch(ENDPOINT, {
method: "POST",
headers: {
"content-type": "application/json",
"x-atlas-key-id": KEY_ID,
"x-atlas-timestamp": ts,
"x-atlas-signature": signature,
},
body,
});
console.log(res.status, await res.json()); cURL (bash + openssl)
KEY_ID="<YOUR_KEY_ID>"
SECRET="<YOUR_SIGNING_SECRET>"
BODY='{"residence":"FR","destinations":["US"],"card":{"network":"visa","tier":"premium"},"market":"fr"}'
TS=$(date +%s)
SIG="v2=$(printf '%s.POST./api/v1/coach/bilan.%s' "$TS" "$BODY" \
| openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)"
curl -s https://atlas.hellosafe.com/api/v1/coach/bilan \
-H "content-type: application/json" \
-H "x-atlas-key-id: $KEY_ID" \
-H "x-atlas-timestamp: $TS" \
-H "x-atlas-signature: $SIG" \
-d "$BODY" Reference
Request body
| Field | Req | Description |
|---|---|---|
residence | yes | Traveller residence, ISO 3166-1 alpha-2. Drives the health rules and the default market. |
destinations | yes | Array of ISO alpha-2 codes. At least 1, up to 50. |
card | no | One card mode (see below). Omit for a no-card analysis. |
trip | no | friends, longTrip, riskyActivity booleans. All default to false. |
market | no | fr · us · ca · sg · my · universal. Derived from residence when omitted. |
ref | no | Your affiliate ref. Sets the attribution on the returned quote link. |
Card modes
Provide exactly one shape under card. To analyse the exact card a traveller holds, send its catalogueId from the card catalogue: it always lands on that very card. A BIN or a bank name + tier also find real catalogue cards, by range: when a bank sells two cards of the same tier, the most basic one wins. On a miss, both fall back to a generic profile.
| Mode | Shape | Resolves to |
|---|---|---|
| Catalogue card | { "catalogueId": 188 } | The exact card contract. Take the id from GET /api/v1/coach/cards. A sandbox key identifies cards by BIN or bank instead. |
| BIN | { "bin": "497010" } | First 6-8 digits of the card. Matched to the real catalogue card, else a generic profile. |
| Bank + tier | { "bank": "BNP Paribas", "network": "visa", "tier": "premium" } | The card read off its face, no BIN. Scoped to the cardholder's residence. Lands on the most basic card of that tier at that bank: byBankTier in the catalogue says which cards it reaches. |
| Generic | { "network": "visa", "tier": "premium" } | Generic profile, no specific card. tier in entry · mid · premium · elite. |
| No card | { "none": true } | Every guarantee reads "not available". |
Tiers
The four tier words, and what they read as on a card. Watch Visa Premier: it is mid.
tier On the card entryVisa Classic, Visa Electron, Mastercard Standard midVisa Premier, Gold Mastercard, Visa Signature premiumVisa Infinite, Visa Platinum, Mastercard Platinum eliteMastercard World Elite, Visa Infinite Privilege
Response
A 200 returns { ok, card, bilan, destinations, offer, meta }. Trimmed example (a BIN matched to a real card):
{
"ok": true,
"card": {
"origin": "exact", // exact | generic | baseline | none
"currency": "EUR",
"detected": {
"bank": "BOURSORAMA BANQUE", "brand": "VISA", "level": "SIGNATURE",
"matched": { "cardId": 6, "cardName": "Visa Premier", "bank": "BoursoBank" }
},
"guarantees": { // the card's GAPS only (adequately-covered ones omitted)
"hospitalFeesAbroad": { "state": "value", "covered": true,
"value": 155000, "recommendedValue": null, "badge": null }
}
},
"bilan": {
"shouldSell": true,
"medical": { "target": "500 000 €", "targetValue": 500000, "ceilingOk": false },
"argumentCount": 9, // count of the sell arguments actually returned
"social": { "pct": 73, "subjectKey": "coach.social.profile" },
"focusCategories": ["sante", "voyage"],
"arguments": [
{
"id": "med_ceiling",
"category": "sante", // sante | voyage | biens | responsabilite | global
"strength": "decisive", // decisive | strong | useful
"figure": "155 000 €", // pre-formatted
"messageKeys": { "hook": "coach.med_ceiling.hook",
"fact": "coach.med_ceiling.fact",
"pitch": "coach.med_ceiling.pitch" },
"vars": { "ceil": "155 000 €", "target": "500 000 €" }
}
]
},
"offer": { "quoteUrl": "https://hellosafe.com/fr/travel-insurance/app?ref=..." },
"meta": { "engineVersion": "1.1.0", "market": "fr", "warnings": [] }
} Rendering the arguments. Each argument's copy is a set of messageKeys plus vars. Interpolate the vars into your own wording for each key, or ask us for the HelloSafe key dictionary. figure and quote are ready-formatted strings.
bilan.medical is the trip's recommended medical ceiling and whether the card meets it (ceilingOk). The target follows the most expensive destination: 30 000 € when every destination is inside the traveller's health zone, 100 000 € for low-cost countries, 150 000 € for mid-cost ones, 300 000 € for high-cost ones (Canada, Japan, Australia…) and 500 000 € for the United States. A destination with no cost data takes the top step.
offer.quoteUrl carries your attribution when your key is set up with it: sales through that link are credited to you automatically.
Reference
Card catalogue
GET /api/v1/coach/cards returns the bank cards the Coach knows, per country of residence: the banks, then each bank's cards, with the name printed on the card, its picture and the catalogueId to send to the bilan. Build your picker from it: the traveller's bank, then their card, then the bilan with that card's catalogueId. When their bank is not listed, fall back to a generic network + tier from the generic block.
The call is free: it does not spend your daily quota. Cards change rarely, so cache the list on your side.
Query Req Description countryno Country of residence, ISO alpha-2. Omit it to get every country with a bank list: FR, US, CA, SG and MY. Any other valid code returns an empty list: use the generic profiles. langno fr or en, for the card names. Default: French for FR, English elsewhere.
import crypto from "node:crypto";
const KEY_ID = "<YOUR_KEY_ID>";
const SIGNING_SECRET = "<YOUR_SIGNING_SECRET>";
// A GET has no body: the signed message ends on the dot.
// The path is signed, the query string is not.
const ts = Math.floor(Date.now() / 1000).toString();
const signature =
"v2=" + crypto.createHmac("sha256", SIGNING_SECRET)
.update(`${ts}.GET./api/v1/coach/cards.`).digest("hex");
const res = await fetch("https://atlas.hellosafe.com/api/v1/coach/cards?country=FR", {
headers: {
"x-atlas-key-id": KEY_ID,
"x-atlas-timestamp": ts,
"x-atlas-signature": signature,
},
});
const { banks } = await res.json();
// The traveller picks a bank, then a card. Send that card to the bilan:
// card: { catalogueId: chosenCard.catalogueId } {
"ok": true,
"country": "FR",
"key": { "keyId": "ak_live_...", "mode": "live",
"cardModes": ["bin", "bank", "catalogueId", "network", "none"] },
"countries": [ // every country with a bank list
{ "code": "FR", "market": "fr", "banks": 28, "cards": 112 },
{ "code": "US", "market": "us", "banks": 19, "cards": 123 }
],
"banks": [
{
"name": "BNP Paribas", "country": "FR", "logoUrl": "https://...",
"cards": [ // most basic first
{ "catalogueId": 197, "name": "Visa Origin", "network": "visa",
"tier": null, "byBankTier": false, "imageUrl": "https://..." },
{ "catalogueId": 187, "name": "Visa Premier BNP Paribas", "network": "visa",
"tier": "mid", "byBankTier": true, "imageUrl": "https://..." },
{ "catalogueId": 188, "name": "Visa Infinite", "network": "visa",
"tier": "premium", "byBankTier": true, "imageUrl": "https://..." }
]
}
],
"generic": [ // the fallback when the bank is not listed
{ "network": "visa", "tiers": ["entry", "mid", "premium", "elite"] },
{ "network": "amex", "tiers": ["mid", "premium", "elite"] }
],
"tiers": [
{ "tier": "mid", "examples": ["Visa Premier", "Gold Mastercard", "Visa Signature"] }
]
} Field What it tells you cards[].catalogueIdThe card, exactly. Send it as card.catalogueId. cards[].tierThe card's range as the Coach reads it from its name: entry, mid, premium, elite, or business. Null when the name carries no range word (Visa Origin, Hello Prime). cards[].byBankTierTrue when bank + network + tier lands on this very card. False when another card of the same tier wins at that bank, or when the name has no range word: only the catalogueId reaches that card. countriesEvery country with a bank list and its bank and card counts, whichever country you asked for. genericThe networks and tiers with a generic profile of their own, for travellers whose bank is not listed. Any other tier on a listed network uses that network's baseline profile. key.cardModesThe card modes your key may send to the bilan. A sandbox key has no catalogueId.
Edge cases
Warnings and errors
Non-fatal issues never fail the call: the Bilan still returns, and a code is added to meta.warnings.
Warning Meaning BIN_LOOKUP_FAILEDBIN could not be looked up; baseline card used. CARD_PROFILE_UNMATCHEDNo card matched; baseline card used. CARD_NOT_FOUNDcatalogueId did not resolve; baseline card used. FORMALITIES_UNAVAILABLEDestination facts unavailable; the Bilan runs without them.
Errors
Shape: { "error": "CODE" }.
HTTP Code Cause 400 BAD_RESIDENCEresidence is not a 2-letter code. 400 NO_DESTINATIONSNo valid destination supplied. 400 BAD_COUNTRYCard catalogue: country is not a 2-letter code. 401 UNAUTHORIZEDMissing / unknown / revoked key. 401 STALE_TIMESTAMPTimestamp outside the 5-minute window. 401 INVALID_SIGNATURESignature does not verify. 401 V1_SCHEME_RETIREDThe signature uses the retired v1 scheme. Sign v2. 403 SCOPE_FORBIDDENKey lacks the coach scope. 403 CARD_MODE_FORBIDDENYour key may not send this card mode. The answer lists the modes it may send. 429 QUOTA_EXCEEDEDThe day's quota is spent. Retry-After says when it resets (UTC midnight). 429 RATE_LIMITEDToo many calls this minute. Retry-After says when to retry.
Good to know
Versioning & limits
The path is versioned (/api/v1/). Structured fields are stable; the set of argument ids and message keys can grow as the engine is tuned, so treat unknown ids and keys as forward-compatible additions. Each key has a daily quota and a per-minute ceiling, and every bilan answer carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. The card catalogue costs nothing. For a high-volume integration, tell us your expected throughput and we will raise the ceiling.