Skip to main content
Every request is signed with HMAC-SHA256. Three headers carry the proof.

Required headers

Building the signature

Sign a string of five lines joined by newlines:
  1. Serialise your payload to a string. Keep that exact string — you will both hash it and send it.
  2. Hash it with SHA-256 and hex-encode the digest, lowercase. That is BODY_SHA256.
  3. Build the five-line string above.
  4. Base64-decode your secret key.
  5. HMAC-SHA256 the five-line string with the decoded secret, and hex-encode the result, lowercase.

Rules that trip people up

  • Method uppercase and exact: POST, GET.
  • Path exact and complete — /api/v1/api_partner/orders. Include the /api prefix; exclude the domain, the protocol, and any query string.
  • Timestamp in seconds, not milliseconds, and within 5 minutes of our clock in either direction.
  • Newlines must be real newline characters, not the two literal characters \ and n.
  • Secret key is Base64 and must be decoded before use — unlike the webhook signing secret, which is not. See Two secrets, two conventions.
BODY_SHA256 must be the hash of the exact bytes you send. Serialise your payload to a string once, hash that string, and send that same string — do not re-serialise the object for the request. A different key order, or added whitespace, produces a different hash and the request is rejected with Invalid signature.
For a request with no body, hash the empty string. There is no shorter format — the raw string always has five lines. The digest of the empty string is a constant:

Two secrets, two conventions

You are issued two signing secrets. They point in opposite directions and are used differently, and mixing them up produces a signature that never matches with nothing in the error to say why. Either can be replaced without affecting the other. This page covers the first column; the second is covered under Verifying the signature.

Examples

All four use the same request body. Substitute the fixed credentials and timestamp from the test vector and any of them will produce the signature shown there.

Test vector

Fixed values to check your implementation against. Useful before you have credentials, and for confirming a change to your signing code did not alter the result.

What we check

On every request, in this order:
  1. All three headers are present.
  2. The timestamp is within 5 minutes of our clock. Generate it fresh per request and keep your server on NTP — clock drift is the usual cause of Request timestamp is too old or invalid.
  3. We rebuild the signature from your secret key and compare it in constant time.

Errors

Authentication failures return 401 Unauthorized:
Error Messages:
  • "Missing required authentication headers." - One or more required headers (X-Esim-Story-Access-Key, X-Esim-Story-Signature, X-Esim-Story-Timestamp) are missing
  • "Request timestamp is too old or invalid." - Timestamp is outside the allowed time window or invalid format
  • "Invalid or missing access key. Please provide a valid X-Esim-Story-Access-Key header." - Access key is incorrect or partner not found
  • "Missing secret key in partner record." - Partner exists but has no secret key configured
  • "Invalid signature." - Signature verification failed. In order of likelihood:
    1. The body sent differs from the body hashed. Re-serialising the payload for the request produces different bytes than the string you hashed. Serialise once, hash that string, send that string.
    2. Raw string built wrong — wrong line order, a literal \n instead of a newline, or the /api prefix missing from the path
    3. Secret key not Base64-decoded before use
    4. Body omitted from the raw string entirely (the digest line is required even when the body is empty)
Last modified on August 10, 2026