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.
| Status | Meaning | Who is holding it |
|---|---|---|
PROVISIONING | Sent is arranging the sender | Sent (nothing is required from you yet) |
ACTION_NEEDED | Something is owed or was rejected | You: read compliance.requirements for the keys, or a resubmission has been asked |
PENDING_REVIEW | Filed with a regulator or carrier | The Campaign Registry or a carrier (nothing either side can hurry) |
ACTIVE | The sender is live and can carry traffic | N/A |
INACTIVE | Had a working sender and no longer does | Sent (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 asmultipart/form-datafiles named after their key. - Your registration came back from review and must be corrected. Sent emails what needs changing; send the corrected
compliance.brandorcompliance.campaignvalues with the samePATCHand 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_codeis a stable code to branch on, fromCHANNEL_001toCHANNEL_011. It is decided by the same logic asstatus, so the two never disagree.reasonis 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."
}| Status | reason_code | Meaning |
|---|---|---|
ACTION_NEEDED | CHANNEL_001 | Information or documents are outstanding or were rejected. On an SMS market, reason names them |
ACTION_NEEDED | CHANNEL_002 | The registration came back from review and must be corrected |
ACTION_NEEDED | CHANNEL_003 to CHANNEL_005 | The RCS registration was rejected, the channel was suspended, or setup failed |
ACTION_NEEDED | CHANNEL_006, CHANNEL_007 | No WhatsApp Business Account is connected, or it has no phone number |
INACTIVE | CHANNEL_008 | The sender was lost: the number was released or its campaign lapsed |
PENDING_REVIEW | CHANNEL_009 | With the registry or the carriers |
PROVISIONING | CHANNEL_010, CHANNEL_011 | Approved 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"| Field | What it carries |
|---|---|
required | Whether this market demands anything at all |
sender_id_pattern | The 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_flowis 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_casestakes one to five entries, each with one to five sample messages.MIXEDandLOW_VOLUMEare refused as use cases:campaign.volumedecides those. Omittingvolumeregisters the campaign at the higher-fee standard tier.sender_valueis 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 whennumber_typeisALPHANUMERIC: leaving it out is a400. Omit it for carrier-assigned types and one is provisioned.area_codespicks where aLOCALorTEN_DLCnumber 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.numbersasks for severalLOCALorTEN_DLCnumbers 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.quantitydefaults to 1, and only one entry may leave outarea_code. It can't be combined witharea_codes, and it isn't available when the profile inherits the organization's campaign. On aLOCALmarket you already hold, it adds the numbers to that market. A request that would take the market over its number limit is a400that states the limit. USTEN_DLCnumbers 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:
| Outstanding | Send |
|---|---|
A brand or campaign value (US TEN_DLC) | compliance, as below |
| A document, in any market | A multipart/form-data file named after the document's key |
| A registration sent back for correction | The 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.
| Body | Effect |
|---|---|
"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": null | The 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 omitted | Left 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
LOCALmarket claims the new numbers from inventory straight away. Whatever inventory can't cover stays pending. - A
TEN_DLCmarket records the numbers and assigns them once its campaign is approved. It needs a campaign of the profile's own: an inherited one is a400.
The response is the market as stored, in the same shape GET returns, so what comes back can be sent back.
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:
| Field | Why |
|---|---|
brand_name | The 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_url | Fetched 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_url | The 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"
}'| Field | What it does |
|---|---|
number | One 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_code | The 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_url | Where 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_calls | Make 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.
| Status | Meaning |
|---|---|
201 | Voice is on for the number. Store callback_secret |
202 | Sandbox: what would have been written |
400 | number 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 |
409 | Another account already carries calls on this number (RESOURCE_007), or no number is available in the area code asked for (BUSINESS_028) |
502 | The 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"
}
}| Field | What it carries |
|---|---|
number | The number, in E.164 format |
status | ACTIVE while the number carries calls, INACTIVE once it was turned off. Nothing provisions, so there is no PROVISIONING here |
default_for_app_calls | Whether 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_url | Where 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" }'| Body | Effect |
|---|---|
"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": true | Makes 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 omitted | Left 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:
| Request | Answer |
|---|---|
default_for_app_calls: false | 400. 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 active | 409 (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 number | 400. Send status: "ACTIVE" in the same call |
| A number you never turned voice on for | 404. 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
| Symptom | Likely cause | Fix |
|---|---|---|
400 "compliance.brand is required…" on adding a TEN_DLC market | No identity stored and none sent | Send compliance.brand as {"inherit": true} or with the identity's fields |
400 listing compliance.campaign.* fields on adding a TEN_DLC market | Dedicated campaign is incomplete: one or more required fields are missing from what you sent and what you already hold | Supply 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-op | Send the field you meant to change, or null to clear it |
400 on PATCH sending compliance | The market registers with no regime, so it has no brand or campaign | Send its documents as multipart files named after their key |
400 on PATCH naming the expected keys | A file's part name matches no document the market asks for | Rename the part to one of the listed keys |
400 "asks for no documents" on PATCH | Files sent to a market that requires none | Remove the files |
422 on PATCH with files | The upload rules refused a file: empty, too large, or an unsupported type | Fix the file and send the request again |
400 on numbers | The list leaves out a number already assigned, or the campaign is inherited | Include every assigned number, or give the profile its own campaign first |
409 on PATCH sending compliance.brand | The identity is filed and no correction was asked for | Omit brand; to change a filed identity, contact support@sent.dm |
400 on campaign.message_flow | Under 40 characters | Describe where opt-in happens, the unchecked box, and that consent is not a condition of purchase |
400 on campaign.use_cases | MIXED or LOW_VOLUME used as a use case | Remove them and set campaign.volume, which decides how the campaign registers |
404 on a market you believe exists | The country and type pair does not name a market this sender has | List with GET /v3/channels/sms first |
409 on add or patch | A replay of a request already in flight, or a conflicting concurrent write | Reuse the same Idempotency-Key and read the market back |
501 from the requirements endpoint | channel=whatsapp or channel=rcs | Only sms is answered today |
400 BUSINESS_022 on POST /v3/channels/voice | The number is not one you hold | List 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 active | Give default_for_app_calls: true to another number first |
Test outcome http_error with status 401 | Your endpoint verified the question with the wrong secret | Use the callback_secret of the number that received the question; rotate it if it was lost |
Market stuck at PROVISIONING | Sent is arranging the sender | Nothing 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 |
Related pages
- Channels - why one send call routes three channels
- Create and activate Sender Profiles via the API - provisioning a profile and its first market
- Connect a WhatsApp Business Account - the Meta authorization flow
- 10DLC Registration - what The Campaign Registry asks for and why
- Compliance and regulations - what Sent enforces and what stays yours
- Answering Calls - what your callback URL is asked and how to answer
- Calls From Your App - voice tokens and the browser SDK
Connect a WhatsApp Business Account (WABA) to Sent
Connect a WhatsApp Business Account to Sent: Meta authorization, choosing the sending number, confirming the channel is live, and the Sender Profile path.
Creating and Managing API Keys
How to create a Sent API key in the dashboard, store it safely in environment variables, verify it authenticates, and rotate or revoke keys independently.