Skip to content
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
    Requirement
    Optional
    Description

    Up to 255 characters. Makes retries safe — see idempotent retries.

Body

  • Name
    APIKey
    Type
    string
    Description

    Your account API key, from Account & Billing → API Keys 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
    Requirement
    Optional
    Description

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

Two body modes

  • Template mode — templateId + variableValues. Works on WhatsApp and SMS; the only mode that can reach WhatsApp.
  • Free-text mode — text 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.


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 the BYO SMS storage rules do not apply to it. It has its own dispatch point instead: the moment just before Akedly calls Meta, after your fee has been reserved.

  • Before the dispatch point, a non-2xx — a refused fee, a missing WABA, a template render error, a quota or spend-cap refusal — releases the key: a same-key retry starts a fresh attempt, and nothing was sent.

  • After it, a definite failure — Meta rejecting the message (502 send_failed, or a provider-specific code), or a 503 about fee or spend-limit reconciliation — is stored and replayed verbatim for 24 hours. A same-key retry returns that stored failure and does not send again. Once the 24 hours pass the key is gone, and a retry is a new send.

  • An unclear outcome is never released. A timeout, a dropped connection, a Meta server error, a Meta reply with no message ID, or a failure after Meta accepted the message is recorded as unknown: the response is 503 IDEMPOTENCY_OUTCOME_UNKNOWN, it is stored with no expiry, and the same key returns it every time. Treat an unclear WhatsApp failure as a support case, not a retry case. A new key, or a retry with no key, is a new send and can deliver the message twice.

  • 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 while the first request still holds the key returns a 409, and which one depends on how far it got. If Akedly has only reserved the key and has not yet started the provider call (before the carrier boundary on BYO SMS, before the dispatch point on WhatsApp), the retry gets 409 IDEMPOTENCY_OUTCOME_UNKNOWN while the reservation is younger than 2 minutes. Once the provider call has started, it gets 409 IDEMPOTENCY_IN_PROGRESS for 2 minutes after that call started. 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 those 2 minutes. A reservation that never started a provider call is treated as abandoned after 2 minutes and reclaimed by the retry, which becomes the live attempt. Back off, then retry with the same key.
  • A BYO SMS reservation past the carrier boundary never returns 409 and is never reclaimed, even inside those 2 minutes: the retry adopts the in-flight record and continues into reconciliation instead of being turned away. A WhatsApp request that has passed its dispatch point is never reclaimed either: if 2 minutes pass with no stored outcome, the retry gets 503 IDEMPOTENCY_OUTCOME_UNKNOWN (stored with no expiry) rather than a second send.

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:

json
{
  "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:

json
{
  "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 or the outcome is unknown (IDEMPOTENCY_OUTCOME_UNKNOWN) — 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.
403API_KEY_SCOPE_DENIEDThe key is limited to some pipelines and this pipeline is not one of them. Use a key whose scope covers it. The check runs before the signature is verified, so nothing was sent, billed or recorded, and a retry fails the same way until the key or the pipeline you send to changes.
403NO_USABLE_API_KEYAkedly could not tie this send to an API key: the request carried no APIKey and the account has no active key whose scope covers this pipeline. Send your APIKey with every request and keep at least one active key. Nothing was sent, billed or recorded, so a retry is harmless but fails the same way until the account has a usable key.

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.
402API_KEY_BUDGET_EXCEEDEDThe API key you sent has reached its spend limit, the optional cumulative EGP limit set on the key (see API keys). Checked on both channels before dispatch, after the pipeline checks above. Nothing was sent and nothing was billed; the response carries the recorded data.transactionID. The key is released, so a retry with the same Idempotency-Key cannot duplicate a message, but it fails the same way until the key's spend limit is raised or you send with another key. Spend limits are enforced only where they are enabled for your account; in preview, usage over the limit is recorded but not blocked.
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 send that Meta definitely rejected — 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 nothing was sent — Meta rejected the message, or the fee rollback failed before Akedly called Meta — and a same-key retry never sends it twice: if Meta had rejected it, the stored 503 is replayed for 24 hours, and if the key was released before the call, the retry is a fresh send. A retry with no key, or a new key, is a new send. See Retrying safely. Contact Akedly if it keeps repeating.
503API_KEY_BUDGET_RECONCILIATION_REQUIREDAkedly could not settle the spend-limit accounting for your API key. It arrives on two paths and the message tells them apart. "This API-key budget edit is still being reconciled" means someone changed the key's spend limit and Akedly is still applying it; this is checked before dispatch, so nothing was sent, and a retry with the same Idempotency-Key after a short backoff is safe. "The provider accepted the utility message, but API-key budget accounting could not confirm it" means the message was accepted for sending (by Meta on WhatsApp, by your provider on SMS) and only the accounting is unfinished. Do not start a new send: keep the same key, which replays the stored response, and never mint a new one. If you cannot tell which message you got, treat the message as sent. Contact Akedly if it keeps repeating, and quote data.transactionID.
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; the WhatsApp equivalent is PROVIDER_OUTCOME_UNKNOWN, below. 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. If the request carried an Idempotency-Key, the response you receive can be 503 IDEMPOTENCY_OUTCOME_UNKNOWN instead of this one. Contact Akedly if it repeats.
503PROVIDER_OUTCOME_UNKNOWNWhatsApp only. Akedly could not confirm whether Meta accepted the message (the call timed out or dropped, or Meta answered without a message ID), so it may already be on its way. Akedly holds the reserved fees while it reconciles, and the response carries data.transactionID. Never start a new send, and never re-send with a new key. What you receive depends on the key: with an Idempotency-Key the response is 503 IDEMPOTENCY_OUTCOME_UNKNOWN instead of this code, it is stored with no expiry, and the same key returns it every time; without a key you get this code, nothing is stored, and a retry is a brand-new send that can deliver twice. Check whether the message arrived and contact Akedly with the transactionID. The BYO SMS equivalent is provider_outcome_unknown, above.
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_PROGRESSThe first request with this key started the provider call (WhatsApp: the dispatch point just before Meta is called; BYO SMS: the fence before the carrier call) less than 2 minutes ago. 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 with the same key. When the window ends with no stored outcome, the retry gets 503 IDEMPOTENCY_OUTCOME_UNKNOWN instead of a second send, because a request that reached its dispatch point is never reclaimed. This 409 is also what a retry gets when it loses the race to reclaim an abandoned reservation. A reservation that has not yet started the provider call returns 409 IDEMPOTENCY_OUTCOME_UNKNOWN instead (below). BYO SMS past the carrier boundary gets no 409 at all: the retry adopts the in-flight record.
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 2-minute 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.
409IDEMPOTENCY_OUTCOME_UNKNOWNA retry with the same key arrived while the first request had only reserved the key and had not yet started the provider call, and the reservation is less than 2 minutes old. Nothing is stored, and the message reads "still being confirmed… contact support before retrying". Retry with the same key after the 2 minutes: if no provider call has started by then, the reservation is treated as abandoned and your retry becomes the live attempt. Contact Akedly if it persists, and never mint a new key.
503IDEMPOTENCY_OUTCOME_UNKNOWNAkedly cannot confirm what the earlier request with this key did, so it will not send again. You see it when a keyed send ended with an unknown provider outcome (WhatsApp PROVIDER_OUTCOME_UNKNOWN, or BYO SMS provider_outcome_unknown), and when a retry arrives after the earlier request's 2-minute in-flight window with no recorded outcome. The outcome is stored with no expiry, so the same key returns this response every time. The message may already be on its way: do not retry automatically, never mint a new key, check whether it arrived and contact Akedly.