Akedly

Send API

One POST call sends a utility message — WhatsApp over your own Business Account, or SMS over your own BYO provider. This page is the reference for the endpoint, its responses, and the errors it returns by name — an unexpected internal failure can still surface as an uncoded 500.


The send call

Send from your backend only — the request carries your API key and is signed with your pipeline's signing secret, which must never reach a browser or mobile app. The Request Signing guide covers the scheme in depth, with reusable helpers.

Headers

  • Name
    Content-Type
    Type
    string
    Description

    application/json.

  • Name
    x-akedly-timestamp
    Type
    string
    Description

    Unix time in milliseconds. Accepted within ±5 minutes of server time. Required when request signing is enabled on the pipeline (the default).

  • Name
    x-akedly-nonce
    Type
    string
    Description

    A unique random value per request. A nonce is single-use per pipeline for 10 minutes — longer than the timestamp window, so a captured request can never be replayed.

  • Name
    x-akedly-signature
    Type
    string
    Description

    Hex HMAC-SHA256 of timestamp.nonce.bodyHash using your request-signing secret, where bodyHash is the hex SHA-256 digest of the exact body bytes you send.

  • Name
    Idempotency-Key
    Type
    string
    Description

    Optional, up to 255 characters. Makes retries safe — see idempotent retries.

Body

  • Name
    APIKey
    Type
    string
    Description

    Your account API key, from Company Profile → View API Key in the dashboard.

  • Name
    pipelineID
    Type
    string
    Description

    The utility pipeline to send through.

  • Name
    phone
    Type
    string
    Description

    Recipient phone number in E.164 (+201234567890). 00 prefixes and Egyptian local 01… forms are accepted and canonicalized.

  • Name
    templateId
    Type
    string
    Description

    The sendable template to render — its ID from your dashboard's Templates list. Template mode — provide this or text, not both.

  • Name
    variableValues
    Type
    object
    Description

    Values for the template's {{variable}} placeholders, keyed by variable name. Keys must match the template's declared variable names exactly — matching is case-sensitive. A variable with no value and no stored fallback fails the send.

  • Name
    text
    Type
    string
    Description

    Free-text mode: the literal message body. SMS-only, so the destination must be covered by your BYO SMS routing.

  • Name
    customerUserId
    Type
    string
    Description

    Optional. Your internal user ID for the recipient; echoed back in the response and recorded with the message.

Request

POST
https://api.akedly.io/api/v1/utilities/send
const crypto = require("crypto");

async function sendUtilityMessage() {
  const body = JSON.stringify({
    APIKey: process.env.AKEDLY_API_KEY,
    pipelineID: "<YOUR_PIPELINE_ID>", // your utility pipeline
    templateId: "<TEMPLATE_ID>", // Order shipped (en)
    variableValues: {
      orderNumber: "1042", // {{orderNumber}}
    },
    phone: "+201234567890", // recipient, E.164
    customerUserId: "user_123", // your internal user id
  });

  // sign the exact body bytes with this pipeline's signing secret
  const timestamp = Date.now().toString();
  const nonce = crypto.randomUUID();
  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/send", {
    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
both modes
{
  "APIKey": "<AKEDLY_API_KEY>",
  "pipelineID": "<YOUR_PIPELINE_ID>",
  "templateId": "<TEMPLATE_ID>",
  "variableValues": {
    "orderNumber": "1042"
  },
  "phone": "+201234567890",
  "customerUserId": "user_123"
}

Two body modes

  • Template modetemplateId + variableValues. Works on WhatsApp and SMS; the only mode that can reach WhatsApp.
  • Free-text modetext instead of a template. SMS-only; useful for content that does not fit a fixed template.

Provide one or the other. A body with neither returns MISSING_TEXT.


Template variables

Template variables are the {{name}} placeholders in a template body; your send call fills them via variableValues (keys are case-sensitive and must match the declared names). Each variable can also carry two optional attributes — set when you author the template.

The whole template body is capped at 1024 characters — checked with your example values when the template is created, the same limit Meta applies to WhatsApp bodies. (SMS segment count grows with the rendered length, but is not a hard cap — and does not affect what you pay: the BYO fee is flat per message.)


Channel resolution

The channel is decided before sending, and a send goes out on exactly one channel — the send call itself never retries on the other channel.

WhatsApp is used when all of these hold:

  1. WhatsApp is enabled on the pipeline
  2. The send is template mode and the template is WhatsApp-usable and Meta-approved
  3. Your WhatsApp Business Account is connected and active
  4. The destination country is in the pipeline's allowed-countries list

SMS is feasible when your BYO routing resolves for the destination. Akedly takes the first feasible channel in your pipeline's channel order — WhatsApp first by default, but an SMS-first order sends SMS even when WhatsApp is also feasible. A destination with no feasible WhatsApp path and no BYO route fails with UTILITY_SMS_REQUIRES_BYO or UTILITY_SMS_COUNTRY_NOT_ROUTED — both 409.


Responses

A 200 means the call was handled — it does not on its own mean a message went out. Read the top-level status for the outcome: success (accepted), skipped (the recipient has opted out — see below), or error.

On success, Akedly accepted the message and recorded it as a transaction. For WhatsApp that is not yet a delivery confirmation — delivery settles asynchronously. For SMS it means your provider accepted the submission and Akedly finished billing it. There is no later delivery settlement: Akedly does not ingest delivery receipts from a BYO provider. Your provider may well publish them — take them from its own dashboard or callbacks, not from Akedly. If the provider accepts an SMS but billing does not complete, you do not get a 200: the endpoint returns 503 provider_outcome_reconciliation_pending instead, so an accepted-but-unbilled SMS always arrives as an error to retry, never as a success to trust. Either way the message is visible in the dashboard's Send History under the returned transactionID. The fields below live under data on accepted sends.

Response fields

  • Name
    transactionID
    Type
    string
    Description

    The message's transaction ID — your handle for tracking it.

  • Name
    status
    Type
    string
    Description

    The message's lifecycle state. For WhatsApp: Pending on acceptance, then Successful on delivery or Failed if delivery fails. For SMS: Successful as soon as your provider accepts the submission — there is no later delivery settlement, because Akedly does not ingest delivery receipts from a BYO chain — your provider may publish them, but nothing here consumes them. Successful on an SMS send means accepted, not delivered.

  • Name
    channel
    Type
    string
    Description

    whatsapp or sms.

  • Name
    provider
    Type
    string
    Description

    own-waba for WhatsApp, own-sms for SMS.

  • Name
    wamid
    Type
    string
    Description

    WhatsApp only — Meta's message ID.

  • Name
    segments
    Type
    number
    Description

    SMS only — how many segments the rendered text occupies. Informational: the BYO fee is flat per message, so this does not change what you are charged.

  • Name
    encoding
    Type
    string
    Description

    SMS only — GSM7 or UCS2.

  • Name
    billed
    Type
    boolean
    Description

    Whether this message has been charged. On a 200 SMS response this is always true — an accepted SMS whose billing did not complete is returned as 503 provider_outcome_reconciliation_pending rather than a success, so billed: false never reaches you on a 200. WhatsApp returns false with billing.status: "deferred" — it bills on the delivery outcome.

  • Name
    customerUserId
    Type
    string
    Description

    Echo of the value you sent, or null.

Response

JSON
200 WhatsApp accepted
{
  "status": "success",
  "message": "Utility WhatsApp accepted",
  "data": {
    "transactionID": "AKDLYTXN…",
    "status": "Pending",
    "channel": "whatsapp",
    "provider": "own-waba",
    "recipient": "+201234567890",
    "customerUserId": "user_123",
    "wamid": "wamid.HBgM…",
    "billed": false,
    "billing": { "status": "deferred" }
  }
}

Response

JSON
200 SMS accepted
{
  "status": "success",
  "message": "Utility SMS accepted",
  "data": {
    "transactionID": "AKDLYTXN…",
    "status": "Successful",
    "channel": "sms",
    "provider": "own-sms",
    "segments": 1,
    "encoding": "GSM7",
    "recipient": "+201234567890",
    "customerUserId": "user_123",
    "billed": true,
    "billing": { "…": "…" }
  }
}

Response

JSON
200 recipient opted out
{
  "status": "skipped",
  "code": "RECIPIENT_SUPPRESSED",
  "message": "Recipient has opted out; message not sent.",
  "data": {
    "recipient": "+201234567890",
    "skipped": true,
    "transactionID": "AKDLYTXN…"
  }
}

Idempotent retries

Send an idempotency key to make retries safer — in the Idempotency-Key header, or as an idempotencyKey field in the request body if a header is inconvenient. Akedly reads the header first and falls back to the body field, for every integration and not just Vodafone; the header wins when both are present. The value must be a string of at most 255 characters. The stored-response reservation uses your key exactly as sent — it is not trimmed, so "a" and " a " reserve separately and a whitespace-only key is reserved like any other. BYO SMS separately trims the key before deriving the identities it sends with — both the transaction identity and the provider's dispatch key — so those two spellings can still converge there; and Vodafone trims it before requiring it, so a whitespace-only key is refused as missing (see below). Send the same exact string every time and none of this can bite you. When Akedly has stored a response for that key, retrying with the same key replays it instead of sending again — but whether a response is stored depends on the channel and on how far the request got, so a key is not on its own a guarantee against a second send.

Beyond that, what a retry actually does depends on the channel and on how far the original request got.

SMS through your own provider (BYO SMS)

  • Akedly marks a carrier boundary the moment it hands your message to your provider's API.

  • Before that boundary, a non-2xx failure releases the key: retrying with the same key starts a fresh attempt. Any 2xx pre-boundary response is stored and replayed for 24 hours — including a 200 with status: "skipped", which is not a send at all.

  • After it, the record no longer expires, and what a retry gets depends on the outcome:

    • Most post-boundary responses — success or error — are stored and replayed verbatim. A retry replays rather than re-attempts. That is deliberate: past that point a new key could submit the message a second time and bill you twice.
    • 503 provider_outcome_reconciliation_pending is the one to think about, and you cannot tell from outside which way it will go. Akedly marks most of these non-replayable on purpose: the stored body is discarded, so a same-key retry re-enters reconciliation and can return a different outcome later — a success, or a 502 send_failed. At least one path does not get that marking and is stored like any other post-boundary response, so the same 503 replays. Do not model either behaviour; keeping the same key is safe both ways. What separates them is progress — an identical 503 returning unchanged over a long period is the stored case, and needs Akedly rather than more retries.
  • All of this needs a key. Idempotency here is opt-in for generic integrations — but Vodafone utility SMS requires one, and a Vodafone send without one is refused with 400 VODAFONE_IDEMPOTENCY_KEY_REQUIRED before anything is dispatched. What is required is the key, not specifically the header — either source above satisfies Vodafone, but the value is trimmed before it is checked, so " " is refused exactly like an absent one. Without a key nothing is stored and nothing replays, so do not retry an unclear outcome automatically. WhatsApp

  • The WhatsApp path sets no carrier boundary, so none of the BYO SMS storage rules apply to it.

  • A failed send is never stored: the key is released and a same-key retry starts a genuinely new attempt.

  • ⚠️ That includes ambiguous failures. A timeout or network error is reported as a send failure even though Meta may already have accepted the message, so retrying can deliver it twice. Treat an unclear WhatsApp failure as a support case, not a retry case.

  • Any 2xx response is stored and replayed for 24 hours — including a 200 with status: "skipped", which is not a send.

Both channels

  • Keys are scoped to your account.
  • Reusing a key with different request parameters returns 422 IDEMPOTENCY_KEY_REUSE. The comparison ignores your API key and the idempotencyKey body field, so changing only those is not a reuse — changing the recipient, the text or the template is.
  • Retrying before the carrier boundary returns 409 IDEMPOTENCY_IN_PROGRESS while the reservation is younger than 60 seconds. The clock, not the first request, is what decides: Akedly cannot see whether that request is still alive, so a retry after a crash is still refused for the rest of the minute, and past it the reservation is treated as abandoned and reclaimed by the retry, which becomes the live attempt. Back off briefly and retry.
  • A BYO SMS reservation past the boundary never returns 409 and is never reclaimed, even inside those 60 seconds: the retry adopts the in-flight record and continues into reconciliation instead of being turned away. ⚠️ WhatsApp never sets that marker, so an expired lease is not evidence that Meta was never reached — do not retry a WhatsApp request automatically just because the 60 seconds elapsed.

Because a retried request is a new HTTP request, sign it with a fresh timestamp and nonce; only the Idempotency-Key stays the same.


Rate limits

Rate limits are configured per pipeline in the dashboard, as per-recipient and per-pipeline windows (per minute, hour, and day). Exceeding one returns 429 with a machine-readable cooldown:

{
  "status": "error",
  "code": "RATE_LIMIT_PHONENUMBER_PERMINUTE",
  "message": "Rate limit exceeded for phoneNumber per minute",
  "retryable": true,
  "retryAfter": "2026-07-14T12:01:00.000Z",
  "cooldownSeconds": 42,
  "details": { "type": "phoneNumber", "window": "perMinute", "max": 1, "current": 1 }
}

The code combines the limited identifier (PHONENUMBER or PIPELINE) with the window (PERMINUTE, PERHOUR, PERDAY). Honor retryAfter — retrying earlier only re-hits the limit.


Error reference

Errors share one structure:

{
  "status": "error",
  "code": "ERROR_CODE",
  "message": "Human-readable description"
}

Rate-limit errors add retryable, retryAfter, cooldownSeconds, and details; template variable errors add missing (SMS) or variable (WhatsApp).

Key limits

LimitValue
Signature timestamp skew±5 minutes
NonceSingle use per pipeline
Idempotency replay window24 hours, unless a BYO SMS request crossed the carrier boundary — then no expiry
Idempotency-Key length255 characters
Rate limitsPer pipeline config (per phone and per pipeline)

Authentication and pipeline errors

StatusCodeCause and Solution
400MISSING_REQUIRED_FIELDSpipelineID is missing from an API-key-authenticated request. If the API key itself is missing, the route returns INVALID_REQUEST_AUTH instead.
401INVALID_API_KEYAPI key does not match any account. Verify it in the dashboard.
404PIPELINE_NOT_FOUNDNo pipeline with that ID, or the pipeline belongs to another account. Check the pipeline's basic details and use one from the same account as the API key.
403NOT_A_UTILITY_PIPELINEThe pipeline is an OTP pipeline. Create a utility pipeline and use its ID.
403PIPELINE_INACTIVEThe pipeline is switched off. Activate it in the dashboard.

Request signing errors

StatusCodeCause and Solution
400SIGNING_NOT_CONFIGUREDSigning is enabled but the pipeline has no signing secret yet. Reveal one in the pipeline's settings.
401MISSING_SIGNATUREOne of the three signing headers is absent. Send x-akedly-timestamp, x-akedly-nonce, and x-akedly-signature.
401SIGNATURE_TIMESTAMP_SKEWTimestamp outside the ±5-minute window. Sync your server clock and send milliseconds, not seconds.
401INVALID_SIGNATURESignature verification failed — usually the body bytes were re-serialized after signing, or the wrong secret was used.
401SIGNATURE_REPLAYThe nonce was already used on this pipeline. Generate a fresh nonce per request.

Request validation errors

StatusCodeCause and Solution
400INVALID_PHONEphone is missing or is not a valid E.164 number after canonicalization.

Template and content errors

StatusCodeCause and Solution
400MISSING_TEXTThe body has neither templateId nor text. Provide one.
404TEMPLATE_NOT_FOUNDNo template with that ID on your account.
409TEMPLATE_NOT_APPROVEDThe template is not Akedly-approved yet. Submit it and wait for approval.
409TEMPLATE_NOT_SMSThe send resolved to SMS but the template is not usable on SMS.
409TEMPLATE_NOT_WHATSAPPThe send resolved to WhatsApp but the template is not enabled for WhatsApp. Enable WhatsApp on the template, or send it on SMS — the SMS route is only an option where your BYO integration covers the destination, which for a managed Vodafone integration means Egypt only.
409TEMPLATE_NOT_WHATSAPP_APPROVEDThe template is enabled for WhatsApp but still awaiting Meta approval, so it can't send on WhatsApp yet.
400TEMPLATE_MISSING_VARIABLESMS render failed: a {{variable}} has no value and no fallback. The missing array lists the names.
400TEMPLATE_VAR_MISSINGWhatsApp render failed: a template variable has no value and no fallback. The variable field names it.

Channel errors

StatusCodeCause and Solution
409UTILITY_SMS_REQUIRES_BYOSMS was the resolved channel but BYO SMS is off for this pipeline, or no integration is eligible in this routing scope. If you build your own, add and test one — see BYO SMS. On a managed Vodafone integration, adding another provider is not the fix: check that BYO SMS routing is on for the pipeline, and that the integration is active with a passing test. An integration that exists but is inactive or has not passed its test is never eligible to be chosen, so it produces this code — not UTILITY_SMS_PROVIDER_NOT_READY. That one applies only after an active, test-passing integration has already been selected and then fails Akedly's send-time readiness check.
409UTILITY_SMS_COUNTRY_NOT_ROUTEDYou have a BYO integration, but no chain routes this destination. On an integration you built, add a per-country chain or an ALL catch-all — see coverage and failure behavior. On a managed Vodafone integration you cannot: its coverage is fixed to Egypt, so this destination is out of scope for it.
409UTILITY_SMS_PROVIDER_NOT_READYToday this code is specific to managed Vodafone: it is the only integration whose readiness Akedly re-checks at send time, and it fails closed rather than falling through to another provider. Do not change or deactivate the integration. By the time you see this code, the integration was already active and test-passed — that is what let it be selected at all — so your own setup is not what is failing. Most of the readiness checks behind this code are Akedly-owned and invisible to you: whether the Vodafone pilot is switched on for your account, whether Akedly's outbound IPs are verified, and whether the adapter endpoint is confirmed. Contact Akedly. Only if the dashboard specifically flags one of them should you revisit credentials, the approved sender name, or the IP-whitelist acknowledgement.
409FREE_TEXT_REQUIRES_SMSA free-text (text) send needs SMS, which is off on this pipeline. Enable SMS, or send an approved WhatsApp template instead.
409NO_CHANNEL_ENABLEDThe pipeline has neither WhatsApp nor SMS enabled. Turn on at least one channel.
403DESTINATION_NOT_ALLOWEDWhatsApp isn't enabled for this destination country on the pipeline. Add the country to the pipeline's allowed destinations.
409WHATSAPP_NOT_CONNECTEDThe send resolved to WhatsApp but no active WABA is connected. Connect your WhatsApp Business Account.
409no_active_wabaNo active WhatsApp Business Account connected. Connect one in the dashboard.
409no_waba_phoneThe connected WABA has no active sender number.
500admin_waba_token_missingAn admin utility send found no system token configured. Akedly-side configuration; nothing was sent.
500waba_token_errorAkedly could not read the WhatsApp credentials for this send. This is an Akedly-side configuration failure, not something you can fix by reconnecting your WABA — contact support. Nothing was sent, so retrying is harmless, but it will keep failing until the configuration is corrected.

Billing and delivery errors

StatusCodeCause and Solution
402UTILITY_FEE_UNAVAILABLEAkedly could not resolve a valid fee for this message before dispatch. Nothing was sent.
402INSUFFICIENT_BALANCEAccount quota or balance exhausted. Top up in the dashboard. Checked on both channels before dispatch, in two stages — but the first stage is not the same on both. On SMS the pre-check already knows the message's flat BYO fee, so it rejects as soon as your available quota will not cover that fee, even while some quota remains. On WhatsApp the fee is not known that early, so the pre-check only rejects an account with no quota left at all. Both channels then make an atomic reservation of this message's own fee, which rejects if it will not fit — so WhatsApp goes through both stages too; it only bills on delivery. One exception, BYO SMS only: a message whose fee resolves to exactly 0 — which needs an Akedly-set integration fee override of 0 — is not balance-checked at all and can send on an account with no quota left, because there is no amount to fail against. WhatsApp cannot reach this case; its fee is clamped to a 0.25 EGP floor.
402SPEND_CAP_EXCEEDEDThe pipeline's daily spend cap is reached. Raise the cap or wait for the daily reset. This gates both channels in two stages: a pre-check that rejects once the day's spend has already reached the cap, then a separate atomic reservation of this message's own fee that rejects if it would carry the day past it. WhatsApp is not exempt — it prices the message and reserves that amount against the cap before dispatching. A BYO-SMS message whose fee resolves to exactly 0 is exempt from both stages and will send after the cap is reached; a zero fee cannot carry the day's spend anywhere. WhatsApp cannot resolve to zero — its fee is clamped to a 0.25 EGP floor.
503UTILITY_BUDGET_RESERVATION_PENDINGAkedly is finalizing this message's fee reservation. On both channels the reservation completes before the message is handed to the provider, so nothing was dispatched. Do not start a new send — retry with the same Idempotency-Key after a backoff, and contact Akedly if it keeps repeating. If the original request carried no key, there is nothing to replay: adding one now starts a new operation rather than resuming the old, so treat the outcome as unconfirmed and check before resending.
503UTILITY_BUDGET_RECONCILIATION_PENDINGAkedly could not finish settling the fee it had reserved. It arrives on three different paths — a reservation rollback that did not complete before dispatch, a BYO SMS send that definitively did not reach your provider, and a WhatsApp dispatch failure whose outcome may be either uncertain or a definite rejection — so do not assume the message's fate from this code alone; read the response body. Do not start a new send. On BYO SMS retry with the same Idempotency-Key — but only if the original request carried one; idempotency on this endpoint is opt-in, and adding a key to a retry starts a new operation instead of resuming the old. On WhatsApp do not retry automatically — the key is released on any non-2xx there, so a retry starts a new send after Meta may already have accepted the original. See Retrying safely. Contact Akedly if it keeps repeating.
503UTILITY_BILLING_UNAVAILABLEWhatsApp leg — this message's fee could not be resolved, checked before the fee is reserved, so nothing was sent. Retry shortly with the same Idempotency-Key; if it keeps repeating it is a pricing-configuration gap rather than a transient failure, and will not clear on its own.
503provider_outcome_reconciliation_pendingBYO SMS only — this code never appears on a WhatsApp send. The code does not tell you whether the message was sent; data.certainty does. Four backend states return it, and that field carries accepted (your provider took it and only billing reconciliation is unfinished — the response says so and carries submissionStatus: "accepted"), not_sent (your provider did not receive it and only the reserved fee is still settling), or unknown. The safe action is the same on all of them: keep the same Idempotency-Key and retry later. One exception — read the message: where the send record itself is missing or incomplete it says contact support, and retrying that adopts the same incomplete record and returns the same 503, so escalate instead. Starting a new send — a new key, or a fresh request because you read this as a failure — is what can put a second message in front of your recipient. If the original request carried no key there is nothing to replay, so check whether the message arrived before retrying at all. Never report delivery on the strength of this code; it can still resolve to 502 send_failed.
502provider_outcome_unknownBYO SMS only — this code never appears on a WhatsApp send. Akedly could not determine whether your provider accepted the submission. Never re-send with a new key — the message may already be on its way. With an Idempotency-Key, a BYO SMS response past the carrier boundary is stored, so a same-key retry replays it rather than re-attempting; without a key nothing is stored, so do not retry automatically. Contact Akedly if it repeats.
502send_failedThe provider rejected the send. WhatsApp failures may carry a provider-specific code instead of send_failed. The response includes the recorded transactionID; nothing is billed.

Idempotency errors

StatusCodeCause and Solution
400INVALID_IDEMPOTENCY_KEYThe key is not a string or exceeds 255 characters.
400VODAFONE_IDEMPOTENCY_KEY_REQUIREDA Vodafone utility SMS send arrived with no idempotency key in either the Idempotency-Key header or the idempotencyKey body field. A key is optional for generic integrations and required here; the send is refused before anything is dispatched.
422IDEMPOTENCY_KEY_REUSESame key, different request parameters. Your API key and the idempotencyKey body field are excluded from the comparison. Use a fresh key for a new request.
409IDEMPOTENCY_IN_PROGRESSA reservation for this key is younger than 60 seconds and has not reached the provider. Not a liveness check — Akedly cannot see whether the original request is still running, so this is returned after a crash too. Back off and retry; past 60 seconds the reservation is treated as abandoned and a retry can reclaim it and become the live attempt, and losing that reclaim race returns this same 409. It is not channel-specific — but WhatsApp never records reaching the provider, so a WhatsApp request stays reclaimable and the 60 seconds elapsing is not evidence that Meta was never reached. BYO SMS stops being reclaimable once it reaches your carrier: past that boundary there is no 409 at all, and the retry adopts the in-flight record instead.
409IDEMPOTENCY_CONFLICTTwo requests raced for the same operation and this one lost the insert. The operation is already being processed. Back off and retry with the same Idempotency-Key — a new key here would start a second send. This one is not the 60-second lease described above — it is a raw duplicate-key collision raised when two requests hit the same operation at once, so there is no window to wait out. Retry after a short backoff.

Was this page helpful?