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.


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 — 00 prefixes, Egyptian local 01… — 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": true alongside the counts a real upload would produce. Use it to confirm a batch parses before committing a large list. Send the JSON boolean true — 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

POST
https://api.akedly.io/api/v1/utilities/allowlist
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

JSON
upload and preview
{
  "APIKey": "<AKEDLY_API_KEY>",
  "pipelineID": "<YOUR_PIPELINE_ID>",
  "hmacs": [
    "c638f9bf33b95e279de31800542a12517cec60b889c6d1caf4223846a07afe60",
    "a3f8c92e6b41d7f05c1e8b26d94a7310f6e21d94c87bb52e0d3c5a19b48e7f02",
    "7bd410c25e98aa130f6e21d94c87bb52a3f8c92e6b41d7f05c1e8b26d94a7310"
  ],
  "countries": { "EG": 2, "SA": 1 },
  "note": "Q3 loyalty cohort"
}

Response

JSON
200 Status
{
  "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, or null when ineligible.

Request

POST
https://api.akedly.io/api/v1/utilities/allowlist/check
{
  "APIKey": "<AKEDLY_API_KEY>",
  "pipelineID": "<YOUR_PIPELINE_ID>",
  "phone": "+201001112222"
}

Response

JSON
200 Status
{
  "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

POST
https://api.akedly.io/api/v1/utilities/allowlist/key-fingerprint
{
  "APIKey": "<AKEDLY_API_KEY>",
  "pipelineID": "<YOUR_PIPELINE_ID>"
}

Response

JSON
200 Status
{
  "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:

StatusCodeCause and Solution
403FEATURE_NOT_ENABLEDThe 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.
403SIGNING_REQUIREDSigning 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.
400MISSING_HMACShmacs is missing, empty, or not an array. Hash locally and send the digests — plaintext numbers are not accepted here.
400BATCH_TOO_LARGEMore than 50,000 hashes in one request. Split into batches.
400HASH_SECRET_NOT_SETThe pipeline has no hash secret yet. Reveal it in the Allowlist panel first, hash with it, then upload.
400INVALID_PHONE/allowlist/check only — the number is not valid E.164 after canonicalization.
429ALLOWLIST_RATE_LIMITMore 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.


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.

Was this page helpful?