Managing channels

A channel is where a sender can actually send: a market you send SMS in, your WhatsApp account, your RCS agent, a number you take phone calls on. This guide covers reading them, adding them, and clearing what their compliance still owes. For why Sent routes three channels behind one send call, see Channels.

All requests authenticate with the x-api-key header. Send x-profile-id with an organization key to read or change one of your Sender Profiles instead of the calling account. Only organization API keys may send x-profile-id; a profile-scoped key is rejected with 403, and a profile outside your organization returns 404. A profile key is already scoped to its own profile, so it reads and changes that profile's channels with no header at all.

Status lives on the channel

An identity has no lifecycle. What has one is the registration of a sender in a market and the approval of a channel, so a sender live in one country and mid-registration in another reports exactly that, rather than one flattened answer.

StatusMeaningWho is holding it
PROVISIONINGSent is arranging the senderSent (nothing is required from you yet)
ACTION_NEEDEDSomething is owed or was rejectedYou: read compliance.requirements for the keys, or a resubmission has been asked
PENDING_REVIEWFiled with a regulator or carrierThe Campaign Registry or a carrier (nothing either side can hurry)
ACTIVEThe sender is live and can carry trafficN/A
INACTIVEHad a working sender and no longer doesSent (the number was released or the campaign it carried lapsed)

What each status means in practice

PROVISIONING covers two distinct situations that reason_code distinguishes:

  • No registration has started yet. Sent is setting the market up and nothing is required from you (CHANNEL_011).
  • The carriers have elected the campaign and Sent is acquiring a phone number (CHANNEL_010). Requesting specific area codes can extend this window.

ACTION_NEEDED means something you supplied was rejected or something still owed was not sent:

  • One or more compliance fields or documents are outstanding. The response names them (up to three, then a count). Send them with PATCH /v3/channels/sms/{country}/{type}: fields as JSON, documents as multipart/form-data files named after their key.
  • Your registration came back from review and must be corrected. Sent emails what needs changing; send the corrected compliance.brand or compliance.campaign values with the same PATCH and Sent reviews the registration again.

Whatever an ACTION_NEEDED market is waiting on, the PATCH on its own path is where you send it. See Correct what it holds.

PENDING_REVIEW means the registration is with a regulator or carrier. For a TEN_DLC market, this state begins as soon as a brand row is stored and continues until the carriers elect the campaign. Nothing either side can do speeds this up.

ACTIVE means the sender is live. Requirements still outstanding against the registration (a field update or resubmission) are communicated separately and do not stop the market from sending.

INACTIVE means the market had a working sender and lost it. Sends in this market will fail. Subscribe to channel.deactivated to be told the moment it happens; the event carries the reason.

reason_code and reason

Every SMS market, the WhatsApp account, and the RCS agent that is not ACTIVE carries reason_code and reason alongside status, on GET /v3/channels, the single-market reads, and Sender Profiles. SMS markets also carry them on the channel webhook, which WhatsApp and RCS don't publish yet. On reads, an ACTIVE channel carries neither; the webhook can still carry reason for an ACTIVE market when the event brought its own explanation.

  • reason_code is a stable code to branch on, from CHANNEL_001 to CHANNEL_011. It is decided by the same logic as status, so the two never disagree.
  • reason is a sentence for a person that names who is holding the channel and what, if anything, you need to do. Its wording may improve, so don't parse it.
{
  "status": "PENDING_REVIEW",
  "reason_code": "CHANNEL_009",
  "reason": "Your registration is awaiting submission to or review by The Campaign Registry. Nothing is needed from you."
}
Statusreason_codeMeaning
ACTION_NEEDEDCHANNEL_001Information or documents are outstanding or were rejected. On an SMS market, reason names them
ACTION_NEEDEDCHANNEL_002The registration came back from review and must be corrected
ACTION_NEEDEDCHANNEL_003 to CHANNEL_005The RCS registration was rejected, the channel was suspended, or setup failed
ACTION_NEEDEDCHANNEL_006, CHANNEL_007No WhatsApp Business Account is connected, or it has no phone number
INACTIVECHANNEL_008The sender was lost: the number was released or its campaign lapsed
PENDING_REVIEWCHANNEL_009With the registry or the carriers
PROVISIONINGCHANNEL_010, CHANNEL_011Approved and acquiring a number, or being set up

On reads, reason is specific to the market: it names the outstanding fields and, when they can all be sent as JSON, the PATCH path to send them to. A read that doesn't check a market's requirements says so in reason and points to GET /v3/channels/sms/{country}/{type}, which does. The response to adding a market carries the general sentence for its status. The full list, with what to do for each, is in the Error Catalog.

reason replaces the note field, which is no longer returned. If you read note, read reason instead, and branch on reason_code rather than on either sentence.

Read every channel at once

GET /v3/channels returns every market you send SMS in, your WhatsApp account, your RCS agent, and the numbers you take phone calls on, each with its status:

curl "https://api.sent.dm/v3/channels" \
  -H "x-api-key: $SENT_API_KEY"

sms is a list with one entry per market. Each entry carries a compliance object naming the country and sender type it answers for. An empty requirements means that market is satisfied; anything listed is what to supply.

The entries under sms are the same ones GET /v3/channels/sms returns, in the same shape a single-market read returns, so a list, a member read, and the all-channels read cannot describe a market differently.

voice lists the numbers you take phone calls on, in the shape GET /v3/channels/voice returns. See Voice.

SMS markets

A market is a country plus a sender type. The pair identifies it everywhere: in the list, in the path, and in the compliance it owes.

Sender types are LOCAL, MOBILE, TEN_DLC, TOLL_FREE, SHORT_CODE, and ALPHANUMERIC. 10DLC is accepted as a synonym of TEN_DLC, and availability varies by country. In a path the type is matched ignoring case, so /v3/channels/sms/us/ten_dlc and /v3/channels/sms/US/TEN_DLC are the same market.

Ask what the market wants

Call GET /v3/compliance/requirements before you add a market. It returns what that market demands, and setup[]: the calls that satisfy it, each with a body you can copy and send as it stands:

curl "https://api.sent.dm/v3/compliance/requirements?channel=sms&country=US&type=TEN_DLC" \
  -H "x-api-key: $SENT_API_KEY"
FieldWhat it carries
requiredWhether this market demands anything at all
sender_id_patternThe values the market accepts as a sender id, where you choose it
requirements[]One entry per value the market asks for, keyed as you write it (brand.legal_name, campaign.message_flow), with its type, format, options, minimum length, and item shape
setup[]The ordered calls that satisfy the requirements, with a body template and any attachments

There is always exactly one setup step, POST /v3/channels/sms, because the identity travels in that call's body rather than needing a separate call. Anything that cannot be a JSON value, such as an uploaded document, is listed under attachments instead of in the body template.

Requirements vary enormously. US TEN_DLC registers a brand and a campaign with The Campaign Registry and asks for a substantial set of values; an alphanumeric sender pre-registers with supporting documents in one country and needs nothing at all in another. This is the same declaration that validates the calls themselves, so it cannot drift from what the API accepts.

channel defaults to sms, which is the only channel answered today. whatsapp and rcs return 501 until their requirements are declared. They are refused rather than answered with an empty requirement set, because empty would say the channel demands nothing, and both demand plenty: WhatsApp a business verification and a display-name review, RCS a carrier approval.

Add the market

POST /v3/channels/sms gives the sender a market, whether it is the first one or the fifth:

curl -X POST "https://api.sent.dm/v3/channels/sms" \
  -H "x-api-key: $ORG_API_KEY" \
  -H "x-profile-id: $PROFILE_ID" \
  -H "Idempotency-Key: add-us-tendlc-tenant-42" \
  -H "Content-Type: application/json" \
  -d '{
    "country": "US",
    "number_type": "TEN_DLC",
    "compliance": {
      "brand": {
        "legal_name": "Tenant 42 Coffee Company LLC",
        "business_name": "Tenant 42 Coffee Co",
        "tax_id": "12-3456789",
        "tax_id_type": "us_ein",
        "entity_type": "PRIVATE_PROFIT",
        "street": "123 Main Street",
        "city": "New York",
        "state": "NY",
        "postal_code": "10001",
        "country": "US",
        "contact_name": "Jane Doe",
        "contact_email": "jane@tenant42.example.com",
        "contact_phone": "+12125550123"
      },
      "campaign": {
        "description": "Order and delivery updates for coffee subscriptions",
        "message_flow": "Customers opt in at checkout on tenant42.example.com by ticking a box that is unchecked by default. Consent is not a condition of purchase.",
        "use_cases": [
          {
            "use_case": "DELIVERY_NOTIFICATION",
            "sample_messages": ["Tenant 42 Coffee: your order #1234 ships today. Reply STOP to opt out."]
          }
        ],
        "opt_in_keywords": "START,YES",
        "opt_out_keywords": "STOP",
        "help_keywords": "HELP",
        "opt_in_message": "Tenant 42 Coffee: you are subscribed to order updates. Reply STOP to opt out, HELP for help.",
        "opt_out_message": "Tenant 42 Coffee: you will receive no further messages.",
        "help_message": "Tenant 42 Coffee: for help, email support@tenant42.example.com. Reply STOP to opt out.",
        "privacy_policy_link": "https://tenant42.example.com/privacy",
        "terms_and_conditions_link": "https://tenant42.example.com/terms"
      }
    }
  }'

Everything the regime wants arrives in compliance, as two declared objects: brand, the identity the registration is filed against, and campaign, with the use case, the consent narrative, the volume, the sample messages, and the keywords. There is nothing to set up first, and nothing is submitted by this call: the rows are what a submission is later assembled from, and filing happens externally.

Send { "inherit": true } in place of either object to run on your organization's:

{
  "country": "US",
  "number_type": "TEN_DLC",
  "compliance": {
    "brand": { "inherit": true },
    "campaign": { "inherit": true }
  }
}

A TEN_DLC market registers with a regime, and whichever half of that registration is yours (not inherited from your organization) must be complete when you add the market. The call checks both brand and campaign:

  • If you supply a dedicated brand, every field the regime requires must be present, either in this request or already stored from a previous call.
  • If you supply a dedicated campaign, the same applies: description, message_flow, use_cases, the keyword and message fields, and the policy links must all be present.
  • A half you run by inheritance ("inherit": true) is not asked for anything.

A missing field on a half you own is a 400 that names each path (for example compliance.campaign.message_flow), not a 202 for a market that could never be submitted to TCR.

A field you stored on an earlier call counts as supplied: you do not need to repeat it. Send only what is genuinely new.

A few field rules worth knowing before you fill the template in:

  • campaign.message_flow is the consent narrative in prose. Under 40 characters is refused, and carriers reject vague descriptions: say where opt-in happens, that the box is unchecked by default, and that consent is not a condition of purchase.
  • campaign.use_cases takes one to five entries, each with one to five sample messages. MIXED and LOW_VOLUME are refused as use cases: campaign.volume decides those. Omitting volume registers the campaign at the higher-fee standard tier.
  • sender_value is only for the types where you choose the sender rather than the carrier assigning it (an alphanumeric sender id, a short code, or a number you already own). It is required when number_type is ALPHANUMERIC: leaving it out is a 400. Omit it for carrier-assigned types and one is provisioned.
  • area_codes picks where a LOCAL or TEN_DLC number comes from: one to 50 area codes in your order of preference, for example ["415", "628"], each listed once. Each code is tried in turn and honored exactly: you get a number in a code you named or none yet, never a neighboring code. Omit it for a number anywhere in the country.
  • numbers asks for several LOCAL or TEN_DLC numbers in one call, one entry per area code: [{ "area_code": "212", "quantity": 2 }, { "area_code": "415" }, { "quantity": 1 }] is two numbers in 212, one in 415, and one in any area code. quantity defaults to 1, and only one entry may leave out area_code. It can't be combined with area_codes, and it isn't available when the profile inherits the organization's campaign. On a LOCAL market you already hold, it adds the numbers to that market. A request that would take the market over its number limit is a 400 that states the limit. US TEN_DLC numbers are assigned when the campaign is approved, not at request time.

A market that pre-registers its sender needs its documents in this same call. When the market's setup[] step lists attachments, send the request as multipart/form-data instead of JSON: each request field is its own form field, and each file is a part named after the requirement key it satisfies:

curl -X POST "https://api.sent.dm/v3/channels/sms" \
  -H "x-api-key: $ORG_API_KEY" \
  -H "x-profile-id: $PROFILE_ID" \
  -F "country=$COUNTRY" \
  -F "number_type=ALPHANUMERIC" \
  -F "sender_value=ExmplRetail" \
  -F "senderid_registration_requirements=@sender-id-registration.pdf"

A required document that isn't attached, or a file that is rejected, is a 422 naming each key (for example compliance.senderid_registration_requirements). Nothing is added: the market is refused before it is provisioned, so fix the files and send the whole request again. A JSON body for such a market is refused the same way, because it cannot carry the files.

Read one market

GET /v3/channels/sms/{country}/{type} reads one market: its sender, where that sender stands, and everything its compliance holds:

curl "https://api.sent.dm/v3/channels/sms/us/ten_dlc" \
  -H "x-api-key: $ORG_API_KEY" \
  -H "x-profile-id: $PROFILE_ID"

A market that registers with a compliance regime reports brand and campaign, each carrying inherit to say whether it is yours or your organization's.

A market that registers with nobody has neither, so both keys are absent rather than null. Absent says this market does not ask; null would say it asks and you supplied nothing. It reports documents instead.

requirements lists what is still owed, keyed as you write it. GET /v3/compliance/requirements says what a market demands of anybody; this says what you gave it.

Correct what it holds

PATCH /v3/channels/sms/{country}/{type} supplies whatever a market is waiting on, in any market you hold:

OutstandingSend
A brand or campaign value (US TEN_DLC)compliance, as below
A document, in any marketA multipart/form-data file named after the document's key
A registration sent back for correctionThe corrected compliance.brand or compliance.campaign values, or both
Different numbers (LOCAL or TEN_DLC)numbers, the full list the market asks for from now on

For a US TEN_DLC market, it takes the compliance object from the read straight back:

curl -X PATCH "https://api.sent.dm/v3/channels/sms/us/ten_dlc" \
  -H "x-api-key: $ORG_API_KEY" \
  -H "x-profile-id: $PROFILE_ID" \
  -H "Content-Type: application/json" \
  -d '{ "compliance": { "brand": { "contact_phone": "+12125550123" } } }'

Partial at every level. An omitted key is left alone; an explicit null clears it. Those are different requests, so {} changes nothing and is refused rather than silently accepted, and correcting one field does not blank the others. A requirements key is the path into the body that clears it.

BodyEffect
"brand": { … }Supplies or corrects a dedicated identity for this sender
"brand": { "inherit": true }Runs on your organization's identity, giving up the dedicated one
"brand": nullThe same thing, said the short way
"campaign": { … }Supplies or corrects a dedicated campaign for this sender
"campaign": { "inherit": true }Runs on your organization's campaign
Key omittedLeft exactly as it is

Nothing here creates a second anything (one identity and one campaign per market), so a write is an upsert and a replayed request lands on the row it already wrote. There is no sub-resource for either half: both are singletons of the market, so both are written here.

compliance only applies to a market that registers with a regime. Every other market has no brand or campaign, so compliance there is a 400: its compliance is documents.

A filed identity is 409, unless it was sent back for correction. Once the brand is filed, changing it locally would leave it out of step with the filed copy, so compliance.brand is refused. The campaign is not frozen: omit brand and the same body is accepted. When the market reports ACTION_NEEDED because the registration came back from review, the identity is accepted too, and the correction sends the registration back to Sent for review.

Documents travel as multipart/form-data, each file in a part named after the compliance key it satisfies (the key requirements names). compliance and numbers can ride in the same form as text parts holding their JSON:

curl -X PATCH "https://api.sent.dm/v3/channels/sms/xk/alphanumeric" \
  -H "x-api-key: $ORG_API_KEY" \
  -H "x-profile-id: $PROFILE_ID" \
  -F "business_registration=@./business-registration.pdf"

A file whose name matches no document the market asks for is a 400 that lists the keys that would. A file the upload rules refuse (empty, too large, or an unsupported type) is a 422. If a file fails to store, the call is a 500 and nothing else in the request is applied.

Numbers take the same list as POST /v3/channels/sms, and replace what the market asks for: send the full list you want, not a difference.

{ "numbers": [ { "area_code": "205", "quantity": 2 }, { "area_code": "334", "quantity": 1 } ] }
  • Numbers already assigned stay. A list that leaves one of them out is a 400, because this call never releases a number.
  • A LOCAL market claims the new numbers from inventory straight away. Whatever inventory can't cover stays pending.
  • A TEN_DLC market records the numbers and assigns them once its campaign is approved. It needs a campaign of the profile's own: an inherited one is a 400.

The response is the market as stored, in the same shape GET returns, so what comes back can be sent back.

WhatsApp

POST /v3/channels/whatsapp connects a WhatsApp Business Account that your organization already holds through a Multi-Partner Solution. It takes two fields, waba_id and an optional phone_number_id; the solution, the owning business portfolio, and the access token are all resolved from your organization rather than accepted from you.

See Connect a WhatsApp Business Account for that call in full, including why the token is not a field.

WhatsApp reports ACTION_NEEDED while there is something for you to do: no business account is connected (CHANNEL_006), or the connected account has no phone number registered (CHANNEL_007). Both used to report PROVISIONING.

RCS

POST /v3/channels/rcs asks for an RCS agent and records the order form the carriers vet it against. It always returns 202: an agent is approved by the carriers, not by this call, and nothing here can make that happen sooner.

Three fields are required, because US carriers refuse a submission without them:

FieldWhy
brand_nameThe brand this agent presents. Not taken from compliance.brand: a business may present a different brand on RCS than the one its 10DLC registration is filed against, and the carriers vet this one.
privacy_policy_urlFetched over the public internet at review time, so it must be an absolute http(s) URL. One behind a login passes this call and fails the review.
terms_and_conditions_urlThe same.

Everything else is optional here and can follow as the submission is assembled: display_name, description, agent_use_case, brand_color, the contact details, official_address, the consent narrative, and the start, help, and stop messages.

Two fields cannot be changed casually. hosting_region is us or eu, defaults to us, and cannot be changed once the agent is approved. billing_category is CONVERSATIONAL, SINGLE_MESSAGE, or BASIC_MESSAGE, and defaults to CONVERSATIONAL.

Voice

Voice is per number the way SMS is per market: each number you hold can carry phone calls on its own, with its own callback URL, where Sent asks what to do with every call on it. Nothing registers, so a number carries calls as soon as it is turned on. One of your voice numbers is the default line for calls placed from your app.

Phone calls must be enabled for your account first. This is set by Sent: contact support@sent.dm to request it. What your callback URL is asked, and how to answer, is in Answering Calls.

Turn calls on for a number

POST /v3/channels/voice adds voice to a number you hold, or gives you a new one:

curl -X POST "https://api.sent.dm/v3/channels/voice" \
  -H "x-api-key: $SENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "number": "+12125550100",
    "callback_url": "https://example.com/voice"
  }'
FieldWhat it does
numberOne of your numbers, in E.164 format. Leave the field out to be given a new US number instead. Sending it empty is refused rather than read as a request for a new number
area_codeThe US area code a new number should be in, such as 212. Only for a request that leaves number out; sending both is refused. Omit it too and the number comes from anywhere in the country
callback_urlWhere Sent asks what to do with each call on this number: an absolute HTTP or HTTPS URL on a public host. Required
default_for_app_callsMake this the line calls placed from your app use when a voice token names no number. Omit it and your first voice number takes that role; a later one leaves it where it is

The response is the number as stored plus callback_secret, the whsec_ secret every question to the callback URL is signed with. It is shown here and by the rotate endpoint, nowhere else, so store it now.

{
  "success": true,
  "data": {
    "callback_secret": "whsec_MWYyZDFlMmU2N2RmNmI3YThmNTZhODFiYzJlNGQzYjI=",
    "number": "+12125550100",
    "status": "ACTIVE",
    "default_for_app_calls": true,
    "callback_url": "https://example.com/voice",
    "created_at": "2026-07-22T12:00:00+00:00",
    "updated_at": "2026-07-22T12:00:00+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "req_7X9zKp2jDw",
    "timestamp": "2026-07-22T12:00:00+00:00",
    "version": "v3"
  }
}

A number you turned off earlier is turned back on. The same number with a different callback_url has its URL replaced and keeps its secret. With sandbox: true the request is validated and a simulated number reported with 202; nothing is written and no number is bought.

StatusMeaning
201Voice is on for the number. Store callback_secret
202Sandbox: what would have been written
400number is not a phone number or not one of yours (BUSINESS_022), number and area_code were both sent, or callback_url is not a public HTTP or HTTPS URL
409Another account already carries calls on this number (RESOURCE_007), or no number is available in the area code asked for (BUSINESS_028)
502The number could not be set up to receive calls; nothing was written, repeat the request (BUSINESS_027)

Read a voice number

GET /v3/channels/voice lists every number you turned calls on for, active or inactive, oldest first. GET /v3/channels/voice/{number} reads one. The number goes in the path in E.164 format with the plus sign URL-encoded as %2B. The signing secret is never on a read.

curl "https://api.sent.dm/v3/channels/voice/%2B12125550100" \
  -H "x-api-key: $SENT_API_KEY"
{
  "success": true,
  "data": {
    "number": "+12125550100",
    "status": "ACTIVE",
    "default_for_app_calls": true,
    "callback_url": "https://example.com/voice",
    "created_at": "2026-07-22T12:00:00+00:00",
    "updated_at": "2026-07-22T12:00:00+00:00"
  },
  "meta": {
    "request_id": "req_8H3jNq5sWx",
    "timestamp": "2026-07-22T12:00:00+00:00",
    "version": "v3"
  }
}
FieldWhat it carries
numberThe number, in E.164 format
statusACTIVE while the number carries calls, INACTIVE once it was turned off. Nothing provisions, so there is no PROVISIONING here
default_for_app_callsWhether this is the line calls placed from your app use when a voice token names no number. Exactly one active voice number carries it while you have any
callback_urlWhere Sent asks what to do with each call on this number

A number you never turned voice on for answers 404 (RESOURCE_013).

Change a voice number

PATCH /v3/channels/voice/{number} changes one voice number and answers with the number as stored, the same shape the read returns, so what comes back can be sent back:

curl -X PATCH "https://api.sent.dm/v3/channels/voice/%2B12125550100" \
  -H "x-api-key: $SENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "callback_url": "https://example.com/voice-v2" }'
BodyEffect
"status": "ACTIVE"Turns calls on again for a number you turned off. The callback URL and the secret it had are kept
"status": "INACTIVE"Turns calls off. The callback URL and the secret stay on the number
"default_for_app_calls": trueMakes this the line calls placed from your app use
"callback_url": "https://..."Replaces where Sent asks what to do with each call. The secret is kept, and a number that was waiting for its first URL is turned on
key omittedLeft exactly as it is

status is matched ignoring case, and any combination is accepted. A body that names none of the three is refused with 400. A number added without a callback_url, for example from the dashboard, is INACTIVE for that one reason, so sending it a callback_url turns it on by itself, and it becomes your default line if you have no other active voice number.

What it refuses:

RequestAnswer
default_for_app_calls: false400. An account with active voice numbers always has exactly one default, so the default moves by giving it to another number
status: "INACTIVE" on the default line while other voice numbers are active409 (CONFLICT_007). Move the default first. Turning off your last voice number is allowed; that turns phone calls off
default_for_app_calls: true on an inactive number400. Send status: "ACTIVE" in the same call
A number you never turned voice on for404. Add it with POST /v3/channels/voice

With sandbox: true nothing is written: the request is validated against the stored number and the number is reported with 200 as it would read after the change.

Rotate the secret

POST /v3/channels/voice/{number}/rotate-secret generates a new signing secret for the questions sent to this number's callback URL and returns it. The previous secret stops signing the moment the response is returned, so update your backend before the next call reaches it.

curl -X POST "https://api.sent.dm/v3/channels/voice/%2B12125550100/rotate-secret" \
  -H "x-api-key: $SENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
{
  "success": true,
  "data": {
    "callback_secret": "whsec_MWYyZDFlMmU2N2RmNmI3YThmNTZhODFiYzJlNGQzYjI="
  },
  "error": null,
  "meta": {
    "request_id": "req_7X9zKp2jDw",
    "timestamp": "2026-07-22T12:00:00+00:00",
    "version": "v3"
  }
}

With sandbox: true a secret is generated and returned with 202, and nothing is written. A number that never had voice turned on answers 400 (BUSINESS_024).

Test the callback URL

POST /v3/channels/voice/{number}/test sends a synthetic call.request question, flagged "test": true and signed with the number's real secret, to its callback URL and reports what came back. No call is placed, nothing is billed, and nothing is stored. It makes one attempt with the same deadline as a live call, and no retry.

curl -X POST "https://api.sent.dm/v3/channels/voice/%2B12125550100/test" \
  -H "x-api-key: $SENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

outcome is ok when your endpoint answered 2xx with a valid answer, otherwise timeout, connection_failed, http_error or invalid_answer. The response carries the exact request your endpoint received, headers and body, what it answered, your answer as Sent read it, and for a bad answer the reason, the path of the field at fault and what to fix. Run it before the first call, and again after every change to your callback. The full walk-through, with a response, is in Test Before the First Call.

With sandbox: true nothing is sent: the verdict comes back ok with 202. An account with no active voice number answers 403 (BUSINESS_023).

Troubleshooting

SymptomLikely causeFix
400 "compliance.brand is required…" on adding a TEN_DLC marketNo identity stored and none sentSend compliance.brand as {"inherit": true} or with the identity's fields
400 listing compliance.campaign.* fields on adding a TEN_DLC marketDedicated campaign is incomplete: one or more required fields are missing from what you sent and what you already holdSupply the missing fields in this request, or switch to "campaign": {"inherit": true} to run on your organization's campaign
400 on PATCH with an empty body{} is refused rather than treated as a no-opSend the field you meant to change, or null to clear it
400 on PATCH sending complianceThe market registers with no regime, so it has no brand or campaignSend its documents as multipart files named after their key
400 on PATCH naming the expected keysA file's part name matches no document the market asks forRename the part to one of the listed keys
400 "asks for no documents" on PATCHFiles sent to a market that requires noneRemove the files
422 on PATCH with filesThe upload rules refused a file: empty, too large, or an unsupported typeFix the file and send the request again
400 on numbersThe list leaves out a number already assigned, or the campaign is inheritedInclude every assigned number, or give the profile its own campaign first
409 on PATCH sending compliance.brandThe identity is filed and no correction was asked forOmit brand; to change a filed identity, contact support@sent.dm
400 on campaign.message_flowUnder 40 charactersDescribe where opt-in happens, the unchecked box, and that consent is not a condition of purchase
400 on campaign.use_casesMIXED or LOW_VOLUME used as a use caseRemove them and set campaign.volume, which decides how the campaign registers
404 on a market you believe existsThe country and type pair does not name a market this sender hasList with GET /v3/channels/sms first
409 on add or patchA replay of a request already in flight, or a conflicting concurrent writeReuse the same Idempotency-Key and read the market back
501 from the requirements endpointchannel=whatsapp or channel=rcsOnly sms is answered today
400 BUSINESS_022 on POST /v3/channels/voiceThe number is not one you holdList your numbers with GET /v3/channels, or leave number out to be given one
409 CONFLICT_007 on PATCH /v3/channels/voice/{number}Turning off the default line while other voice numbers are activeGive default_for_app_calls: true to another number first
Test outcome http_error with status 401Your endpoint verified the question with the wrong secretUse the callback_secret of the number that received the question; rotate it if it was lost
Market stuck at PROVISIONINGSent is arranging the senderNothing is owed by you; ACTION_NEEDED is the status that asks for something. A TEN_DLC market reads PENDING_REVIEW once its brand is stored, so PROVISIONING there means a number has been assigned but is not active yet

On this page