Allowlist
The allowlist makes recipients eligible for utility messages without Akedly ever seeing your contact list. Numbers are hashed on your device — in your browser or on your own server — and only the hashes are uploaded. Your plaintext numbers never leave your device.
All allowlist endpoints require request signing
Unlike /send, the allowlist endpoints are fail-closed: they never run unsigned. A pipeline with signing disabled is rejected with SIGNING_REQUIRED instead of falling back to API-key-only. See Request Signing.
The programmatic door needs a verified account
The dashboard's Allowlist panel works on every utility pipeline. The API endpoints on this page — and the downloadable scripts, which call them — additionally require the allowlist API entitlement, which is granted automatically the first time your account is verified. Until then all three return 403 FEATURE_NOT_ENABLED.
How the allowlist works
Each pipeline stores a set of HMAC digests. At send time, Akedly canonicalizes the recipient's number, computes the same HMAC with the pipeline's hash secret, and checks membership — a match is the allowlist eligibility proof.
Because the server keeps the secret, a leaked database of hashes cannot be reversed or brute-forced into phone numbers, and hashes from one pipeline never match another pipeline's list.
Enable the allowlist proof in the pipeline's Audience settings; upload and manage the list from the Allowlist panel or the API below.
Upload from the dashboard
The primary path is your pipeline's Allowlist panel: paste your numbers and they are hashed in your browser, on your device, before anything is sent. Your numbers never leave your device — what is uploaded is the list of 64-hex digests, never a phone number.
Paste from anywhere — one number per line, commas also split (paste-only by design; there is no file upload). The panel canonicalizes, deduplicates, hashes with your pipeline's hash secret, uploads the digests, and shows the added / skipped / invalid outcome.
For very large lists, automation, or high-assurance workflows, use the downloadable script — the same device-local hashing, run from your terminal.
Hashing rules
A hash is HMAC-SHA256(canonicalE164, hashSecret), hex-encoded — 64 hex characters per number.
- Name
canonicalE164- Type
- string
- Description
The number in canonical E.164:
+followed by country code and digits (+201234567890). Equivalent forms —00prefixes, Egyptian local01…— canonicalize to the same string, so always canonicalize before hashing.
- Name
hashSecret- Type
- string
- Description
The pipeline's hash secret (
uhs_…), used as the HMAC key exactly as displayed. Revealed once in the pipeline's Allowlist panel. Not the request-signing secret.
Hash one number
const crypto = require("crypto");
const hmac = crypto
.createHmac("sha256", process.env.AKEDLY_HASH_SECRET)
.update("+201234567890") // canonical E.164
.digest("hex");
Pin your implementation against this test vector before uploading — a drifted hash uploads fine but never matches at send time:
Test vector
HMAC-SHA256(key="uhs_selftest", message="+201234567890")
= c638f9bf33b95e279de31800542a12517cec60b889c6d1caf4223846a07afe60
Upload with the script
The easiest path: your pipeline's Allowlist panel offers a ready-made script — hash-allowlist.js (Node.js), hash_allowlist.py (Python), or hash_allowlist.php (PHP) — downloaded with your pipeline ID pre-filled. Each canonicalizes, deduplicates, hashes, self-tests against the vector above, and refuses to run if its canonicalization drifts from Akedly's. The Node.js script is the full tool — it adds --check verification, the hash-key preflight, and batch splitting; the Python and PHP ports cover hashing and upload.
Workflow
# 1. Dry run — parse, canonicalize, hash; print the hashes, upload nothing
node hash-allowlist.js numbers.csv
# 2. Upload — preflights your hash key, then uploads in batches
node hash-allowlist.js numbers.csv --upload
# 3. Verify — ask Akedly if a number is now eligible
node hash-allowlist.js --check +201001112222
The input file is plain text (.csv or .txt) — one number per line; commas and semicolons also split; blank lines are ignored.
The scripts call the API endpoints below, so they need a verified account: without the entitlement, --upload (including its key-fingerprint preflight) and --check both return 403 FEATURE_NOT_ENABLED. Hashing a file without --upload is entirely local and works regardless.
Configure the script through environment variables:
- Name
AKEDLY_API_KEY- Type
- env
- Description
Your account API key.
- Name
AKEDLY_HASH_SECRET- Type
- env
- Description
The pipeline's hash secret (
uhs_…).
- Name
AKEDLY_SIGNING_SECRET- Type
- env
- Description
The pipeline's request-signing secret (
usk_…) — the upload and check calls are signed.
- Name
AKEDLY_PIPELINE_ID- Type
- env
- Description
Your utility pipeline ID. Pre-filled when you download the script from the pipeline's panel.
Before uploading, the Node.js script compares a fingerprint of your local hash secret against the pipeline's current one and hard-stops on mismatch — catching the silent failure where hashes computed with a stale secret upload successfully but never match. With the Python or PHP port, run the preflight yourself via the key-fingerprint endpoint.
Ingest hashes
Upserts hashes into the pipeline's allowlist. Existing hashes are skipped, malformed entries are counted as invalid, and the response reports the resulting list size.
Body
- Name
APIKey- Type
- string
- Description
Your account API key.
- Name
pipelineID- Type
- string
- Description
The utility pipeline whose list to update.
- Name
hmacs- Type
- string[]
- Description
Hex HMAC-SHA256 digests (64 hex characters each). Up to 50,000 per request — split larger uploads into batches.
- Name
countries- Type
- object
- Description
Optional. Map of ISO 3166-1 alpha-2 codes to counts (
{"EG": 4200, "SA": 800}) describing the upload's composition — powers the dashboard's country breakdown. Not stored per number.
- Name
dryRun- Type
- boolean
- Description
Optional. Preview only: validates and counts the batch against the current list without writing anything. The response echoes
"dryRun": truealongside the counts a real upload would produce. Use it to confirm a batch parses before committing a large list. Send the JSON booleantrue— the string"true"is not a preview and the upload is performed for real.
- Name
note- Type
- string
- Description
Optional, trimmed to 120 characters. A label for this upload, shown in the panel's upload history.
Response fields
- Name
added- Type
- number
- Description
New hashes inserted.
- Name
skipped- Type
- number
- Description
Hashes already on the list.
- Name
invalid- Type
- number
- Description
Entries rejected for not being 64-hex strings.
- Name
total- Type
- number
- Description
List size after the upload.
Request
const crypto = require("crypto");
async function uploadAllowlist(hmacs) {
const body = JSON.stringify({
APIKey: process.env.AKEDLY_API_KEY,
pipelineID: process.env.AKEDLY_PIPELINE_ID,
hmacs, // 64-hex digests, hashed on your machine
});
// sign the exact body bytes with the pipeline's SIGNING secret (usk_), not the hash secret
const timestamp = Date.now().toString();
const nonce = crypto.randomBytes(16).toString("hex");
const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
const signature = crypto
.createHmac("sha256", process.env.AKEDLY_SIGNING_SECRET)
.update(`${timestamp}.${nonce}.${bodyHash}`)
.digest("hex");
const res = await fetch("https://api.akedly.io/api/v1/utilities/allowlist", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-akedly-timestamp": timestamp,
"x-akedly-nonce": nonce,
"x-akedly-signature": signature,
},
body,
});
return res.json();
}
Body only
{
"APIKey": "<AKEDLY_API_KEY>",
"pipelineID": "<YOUR_PIPELINE_ID>",
"hmacs": [
"c638f9bf33b95e279de31800542a12517cec60b889c6d1caf4223846a07afe60",
"a3f8c92e6b41d7f05c1e8b26d94a7310f6e21d94c87bb52e0d3c5a19b48e7f02",
"7bd410c25e98aa130f6e21d94c87bb52a3f8c92e6b41d7f05c1e8b26d94a7310"
],
"countries": { "EG": 2, "SA": 1 },
"note": "Q3 loyalty cohort"
}
Response
{
"status": "success",
"message": "Allowlist updated",
"data": {
"added": 4980,
"skipped": 15,
"invalid": 5,
"total": 12480
}
}
Check eligibility
Runs a single number through the exact eligibility gate /send uses and reports the verdict. Nothing is stored. Use it to confirm an upload worked — note this call sends the plaintext number, the same exposure as a real send.
Body
- Name
APIKey- Type
- string
- Description
Your account API key.
- Name
pipelineID- Type
- string
- Description
The pipeline to check against.
- Name
phone- Type
- string
- Description
The number to check, E.164. Canonicalized before checking.
Response fields
- Name
eligible- Type
- boolean
- Description
Whether a send to this number would pass the eligibility gate right now.
- Name
proof- Type
- string | null
- Description
Which proof matched:
verified,allowlist, ornullwhen ineligible.
Request
{
"APIKey": "<AKEDLY_API_KEY>",
"pipelineID": "<YOUR_PIPELINE_ID>",
"phone": "+201001112222"
}
Response
{
"status": "success",
"data": {
"eligible": true,
"proof": "allowlist"
}
}
Verify your hash key
Returns a short fingerprint of the pipeline's current hash secret — the first 8 hex characters of SHA-256(hashSecret). Compare it against the same computation on your local secret before a bulk upload; a mismatch means you hold a stale secret and your hashes would never match.
The downloadable Node.js script performs this preflight automatically on --upload.
Body
- Name
APIKey- Type
- string
- Description
Your account API key.
- Name
pipelineID- Type
- string
- Description
The pipeline whose hash key to fingerprint.
Request
{
"APIKey": "<AKEDLY_API_KEY>",
"pipelineID": "<YOUR_PIPELINE_ID>"
}
Response
{
"status": "success",
"data": {
"fingerprint": "3b7f0a2c"
}
}
Limits and errors
Allowlist endpoints share a burst guard: 100 requests per minute per pipeline. A 429 includes a Retry-After header and cooldownSeconds.
Authentication and pipeline errors (INVALID_API_KEY, PIPELINE_NOT_FOUND, NOT_A_UTILITY_PIPELINE, …) match the send error reference. Allowlist-specific errors:
| Status | Code | Cause and Solution |
|---|---|---|
| 403 | FEATURE_NOT_ENABLED | The account lacks the allowlist API entitlement, granted automatically on account verification — verify your account first. The gate covers all three endpoints on this page and the downloadable scripts; the dashboard's Allowlist panel is unaffected. If your account is already verified, contact Akedly. |
| 403 | SIGNING_REQUIRED | Signing is disabled (or no signing secret is set) on this pipeline. Enable signing, then sign the request — the allowlist door never falls back to API-key-only. Unsigned requests on a signing-enabled pipeline return 401 MISSING_SIGNATURE instead. |
| 400 | MISSING_HMACS | hmacs is missing, empty, or not an array. Hash locally and send the digests — plaintext numbers are not accepted here. |
| 400 | BATCH_TOO_LARGE | More than 50,000 hashes in one request. Split into batches. |
| 400 | HASH_SECRET_NOT_SET | The pipeline has no hash secret yet. Reveal it in the Allowlist panel first, hash with it, then upload. |
| 400 | INVALID_PHONE | /allowlist/check only — the number is not valid E.164 after canonicalization. |
| 429 | ALLOWLIST_RATE_LIMIT | More than 100 allowlist requests in a minute on this pipeline. Honor Retry-After. |
Rotating the hash secret
Regenerating the hash secret clears the pipeline's allowlist — entries hashed with the old secret can never match again, so Akedly removes them rather than leave dead rows.
Rotation means re-upload
After regenerating: update AKEDLY_HASH_SECRET wherever it is stored, re-hash your full list with the new secret, and re-upload. Until then, recipients relying on the allowlist proof are not eligible. The verified proof is unaffected.
Next: send to them
Once numbers are eligible, deliver with the Send API, or attach your own user IDs to eligible numbers with End Users.