Authentication
Sign every request with your API key and secret using HMAC-SHA256.
Every request to a Dynamo API is signed. There are no cookies or bearer tokens — you authenticate by computing an HMAC-SHA256 signature over the request timestamp with your organization's secret.
You need two credentials, both issued per organization:
- API key — a public identifier (a document id), sent as
x-api-key. - Secret — used only to sign requests; never sent over the wire.
The External API and the Audience API share the exact same
authentication — the same key/secret and the same signing scheme. They differ
only in the base URL and in x-api-version (required by the External API,
accepted but ignored by the Audience API).
The headers
| Header | Value |
|---|---|
x-api-key | Your API key (the public identifier). |
x-date | The request timestamp formatted as an IMF-fixdate string (GMT, US locale): EEE, dd MMM yyyy HH:mm:ss z — e.g. Sun, 06 Dec 2020 12:59:11 GMT. |
x-signature | HMAC-SHA256(message = x-date, key = secret), hex-encoded. |
x-api-version | The API version you're coding against — currently 1. Required by the External API (a missing or unknown value returns 400); the Audience API accepts and ignores it. |
The signature is computed over the x-date string only — not the body, path, or
any other header. x-api-version is not part of the signature.
Generate the signature
- Format the timestamp. Take the current request time (milliseconds) and format it
as an IMF-fixdate string in GMT, US locale:
EEE, dd MMM yyyy HH:mm:ss z(e.g.Sun, 06 Dec 2020 12:59:11 GMT). In JavaScript this is exactlynew Date().toUTCString(). Use this string as thex-dateheader value. - Sign it. Compute
HMAC-SHA256over thex-datevalue, keyed by your organization secret, and encode the result as a hexadecimal string. Use this as thex-signatureheader value. - Send
x-api-key,x-date,x-signature, andx-api-versionon the request.
The signature has a TTL of 30 seconds — generate x-date immediately before each
request. An expired signature returns 403 Signature expired. A key pair is scoped
to a single organization; every read and write happens within that org.
Signing a request
const crypto = require("crypto");
function authHeaders(apiKey, secret) {
const xDate = new Date().toUTCString(); // e.g. Sun, 06 Dec 2020 12:59:11 GMT
const signature = crypto
.createHmac("sha256", secret)
.update(xDate)
.digest("hex");
return {
"x-api-key": apiKey,
"x-date": xDate,
"x-signature": signature,
"x-api-version": "1", // External API only; ignored by the Audience API
};
}XDATE=$(LC_ALL=C TZ=GMT date "+%a, %d %b %Y %H:%M:%S GMT") # IMF-fixdate
SIG=$(printf '%s' "$XDATE" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -H "x-api-key: $KEY" \
-H "x-date: $XDATE" \
-H "x-signature: $SIG" \
-H "x-api-version: 1" \
"$BASE/channels"The same helper works for both APIs — just point it at the right base URL:
| API | Base URL |
|---|---|
| External API | https://us-central1-seamless-pro.cloudfunctions.net/externalApi/v1 |
| Audience API | https://us-central1-seamless-pro.cloudfunctions.net/audience |
Example
Check your implementation against this known pair — the same x-date and secret must
produce the same signature:
const date = 'Mon, 14 Feb 2022 20:35:03 GMT'; // new Date().toUTCString()
const secret = 'xZcLnmVhtl7MAlEgMnYI';
const signature = CryptoJS.HmacSHA256(date, secret).toString(CryptoJS.enc.Hex);
// bf4a8eea118be91013805e443cc4218e8410df136ed4276da5efb5dcab6812f7Auth error responses
| Status | Meaning |
|---|---|
403 Missing request headers | One of x-api-key / x-date / x-signature is absent. |
403 Invalid signature or api key - Trace 1 | API key not found. |
403 Invalid signature or api key - Trace 2 | Signature does not match. |
403 Signature expired | The signature is older than its 30-second TTL. |
402 Payment Required | The organization is not active. |
400 Missing or unsupported x-api-version header. Supported versions: 1. | (External API) x-api-version is absent or not a supported value. |
API version
Send x-api-version: 1 on every External API request — it's required, and a
missing or unknown value returns 400. Pinning a version means our deploys won't break
you: today the only value is 1, and any future breaking change ships as a new
version (announced here, with Deprecation / Sunset / Link response headers giving a
migration window before an old version is retired). The /v1 in the base URL is the
stable API generation; x-api-version is the revision within it. The Audience API
doesn't require the header — it's accepted and ignored there.