HelloSafe Atlas · Travel Insurance API

Coach API

Send a traveller profile and a bank card reference. Get back a structured bilan: the card's coverage gaps and a ranked set of sell arguments you render in your own product.

POSThttps://atlas.hellosafe.com/api/v1/coach/bilan

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.

Sell-only, structured output. The response is scoped to the insurance pitch: it returns the card's shortcomings (not what it covers well) and destination context only through the insurance lens. Copy comes as message keys plus interpolation variables, not finished sentences. You render the UI on your side (or map the keys to your own wording). Numbers and money figures come pre-formatted. One exception to sell-only: 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

ItemValue
Base URLhttps://atlas.hellosafe.com
BilanPOST /api/v1/coach/bilan
Card catalogueGET /api/v1/coach/cards
KeyA 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.

HeaderValue
x-atlas-key-idYour key id.
x-atlas-timestampCurrent time in unix seconds. Rejected if more than 5 minutes off (replay guard).
x-atlas-signaturev2= followed by the hex HMAC-SHA256 of the message below, keyed with your signing secret.
The signed messagehex HMAC-SHA256, keyed with your 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

node ≥ 18 (built-in fetch + crypto)bilan.mjs
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)

bashrun in a terminal
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

FieldReqDescription
residenceyesTraveller residence, ISO 3166-1 alpha-2. Drives the health rules and the default market.
destinationsyesArray of ISO alpha-2 codes. At least 1, up to 50.
cardnoOne card mode (see below). Omit for a no-card analysis.
tripnofriends, longTrip, riskyActivity booleans. All default to false.
marketnofr · us · ca · sg · my · universal. Derived from residence when omitted.
refnoYour 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.

ModeShapeResolves 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.

tierOn 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):

200 OKapplication/json
{
  "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.

QueryReqDescription
countrynoCountry 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.
langnofr or en, for the card names. Default: French for FR, English elsewhere.
node ≥ 18cards.mjs
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 }
200 OK (trimmed)application/json
{
  "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"] }
  ]
}
FieldWhat 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.

WarningMeaning
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" }.

HTTPCodeCause
400BAD_RESIDENCEresidence is not a 2-letter code.
400NO_DESTINATIONSNo valid destination supplied.
400BAD_COUNTRYCard catalogue: country is not a 2-letter code.
401UNAUTHORIZEDMissing / unknown / revoked key.
401STALE_TIMESTAMPTimestamp outside the 5-minute window.
401INVALID_SIGNATURESignature does not verify.
401V1_SCHEME_RETIREDThe signature uses the retired v1 scheme. Sign v2.
403SCOPE_FORBIDDENKey lacks the coach scope.
403CARD_MODE_FORBIDDENYour key may not send this card mode. The answer lists the modes it may send.
429QUOTA_EXCEEDEDThe day's quota is spent. Retry-After says when it resets (UTC midnight).
429RATE_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.