Create and activate Sender Profiles via the API
This guide shows you how to provision a Sender Profile through the API: create it, decide which resources are dedicated to it and which are inherited, give it a sender, and read what its compliance still owes. It is written for platforms and resellers that onboard tenants programmatically; for the dashboard flow, see Creating a Sender Profile, and for choosing between shared and per-tenant architectures, see Multi-Tenant Architectures.
This guide covers /v3/sender-profiles. The older /v3/profiles endpoints, including the two-step create-then-complete flow with a webhook callback, are deprecated. They still behave exactly as before, so nothing has to change today. See Migrating from /v3/profiles at the end of this page.
Before you start, you need:
- An organization account. Call
GET /v3/me: the responsetypemust beorganization. - An organization API key whose user has the
adminrole in the organization. See roles and permissions for how roles are granted.
All requests authenticate with the x-api-key header.
The minimum request
A profile needs a name and a short name. Everything else (identity, inbox, opt-out list, billing) comes from your organization, so the profile is usable immediately with no compliance work:
curl -X POST "https://api.sent.dm/v3/sender-profiles" \
-H "x-api-key: $ORG_API_KEY" \
-H "Idempotency-Key: create-profile-tenant-42" \
-H "Content-Type: application/json" \
-d '{
"name": "Tenant 42 Coffee Co",
"short_name": "T42COFFEE"
}'| Field | Rule |
|---|---|
name | Required. Up to 200 characters. |
short_name | Required. This is the SMS sender id, so it must be 3–11 characters of letters, numbers, and spaces, with at least one letter. |
description | Optional. |
The response carries the profile, its id, and its api_key. The key is shown once, by create, and is never returned again. Store it before you discard the response. Reads never include it.
The status code tells you whether anything is still in flight:
| Code | Meaning |
|---|---|
201 | The profile is complete on return. This is the wholly inherited case, where nothing needed registering. |
202 | The profile was accepted and something is provisioning. Read its channels for the per-capability status. |
Neither is an error, and neither needs a callback: there is no completion step to run and nothing to wait for before the profile is usable.
Dedicated, inherited, or absent
Every capability is one of three things, and there is no flag per state:
| You want | Send |
|---|---|
| Dedicated to the profile | The capability's fields |
| Inherited from the organization | { "inherit": true }, with nothing else |
| Neither | Omit the block entirely |
There is no "inherit": false that also carries no data: a second way to say the same thing would let a request contradict itself. Inheriting needs no id, because a customer has exactly one brand.
A sender is the exception. A brand and a campaign can be inherited; a number cannot. A profile either has a dedicated sender, through channels.sms, or it has none and terminates through the shared routes for that country.
Billing
{ "billing": { "inherit": true } }Omit billing and nothing is opted into: the profile stays on the storage default, where the profile pays. { "inherit": true } bills the organization. fallback draws on the organization's balance when the profile's runs out, and cannot be combined with inherit: true. Inheriting already bills the organization, so the pair is refused rather than silently resolved.
Giving the profile a sender
Ask the market what it wants
Call GET /v3/compliance/requirements first. It returns what the 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: $ORG_API_KEY"channel defaults to sms, which is the only channel answered today; whatsapp and rcs return 501 until their requirements are declared. Requirements vary enormously by market: US TEN_DLC registers a brand and a campaign with The Campaign Registry and asks for a substantial set of values, while an alphanumeric sender needs supporting documents in one country and nothing at all in another.
The response gives you requirements[] — each entry keyed as you write it, such as brand.legal_name or campaign.message_flow — plus sender_id_pattern for the values the market accepts, and setup[]. 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.
This is the same declaration that validates the calls themselves, so it cannot drift from what the API accepts.
Add the market
You can name the profile's first market at create time:
curl -X POST "https://api.sent.dm/v3/sender-profiles" \
-H "x-api-key: $ORG_API_KEY" \
-H "Idempotency-Key: create-profile-tenant-42" \
-H "Content-Type: application/json" \
-d '{
"name": "Tenant 42 Coffee Co",
"short_name": "T42COFFEE",
"billing": { "inherit": true },
"channels": {
"sms": { "country": "US", "number_type": "TEN_DLC" }
},
"compliance": {
"brand": { "inherit": true },
"campaign": { "inherit": true }
}
}'Or add it afterwards, and add every market after the first, with POST /v3/channels/sms and the x-profile-id header. Create takes one market; the sub-resource takes the rest. See Managing channels for that call in full.
| Field | Rule |
|---|---|
channels.sms.country | Required when channels.sms is present. ISO 3166-1 alpha-2, for example US. |
channels.sms.number_type | Required when channels.sms is present. LOCAL, MOBILE, TEN_DLC, TOLL_FREE, SHORT_CODE, or ALPHANUMERIC. 10DLC is accepted as a synonym of TEN_DLC. Availability varies by country. |
channels.sms.sender_value | Optional, and 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. Omit it for carrier-assigned types and one is provisioned. |
compliance.brand is required when number_type is TEN_DLC, either as { "inherit": true } or as the identity's fields. A 10DLC registration attaches to an identity, and no later call can supply it, so a request without one is a 400 rather than a market left with something outstanding.
The OpenAPI document marks only country as required inside channels.sms. The validator also requires number_type.
Read where the sender stands
GET /v3/sender-profiles/{id} reports every market the profile sends in, each with its own status and outstanding compliance:
curl "https://api.sent.dm/v3/sender-profiles/$PROFILE_ID" \
-H "x-api-key: $ORG_API_KEY"The document mirrors the create request, with three differences that follow from a read not being a request:
channels.smsis a list. Create takes the one market a profile starts in; a profile can go on to send in several.brandcomes back in full, withinheritsaying whose it is. On a request,{ "inherit": true }carries no data because there is nothing to send; here it carries the data, because the point of a read is to say what the profile actually sends under.- There is no
campaignsarray. A campaign is only ever a registration, so what a read can usefully say is whether anything is still owed for it — reported per market, inchannels.sms[].compliance.requirements. A profile on the organization's campaign is asked for none of the campaign fields, so an emptyrequirementsis how an inherited campaign reads.
Status lives on the channel, never on the profile or the brand. A profile live in one country and mid-registration in another reports exactly that:
| Status | Meaning |
|---|---|
PROVISIONING | Sent is arranging the sender; nothing is required from you |
ACTION_NEEDED | Something is owed: read compliance.requirements for the keys, or a resubmission has been asked of you |
PENDING_REVIEW | With a carrier or registry (filed, or assembled and queued for Sent to file), and their answer is what moves it |
ACTIVE | The market can send |
An empty requirements means that market is satisfied. Anything listed is what to supply, keyed as you write it.
A profile belonging to another organization reads as 404 rather than 403: saying which of the two it is would confirm the id exists.
Correct what compliance holds
PATCH /v3/channels/sms/{country}/{type} writes what a market declares, and answers with the market as stored — the same shape GET on that path returns, so what comes back can be sent 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": { "tax_id": "12-3456789" } } }'The write is partial at every level: an omitted key is left alone, and an explicit null clears it. Those are different requests, so {} changes nothing and is refused rather than silently accepted. Correcting one field does not blank the others. A requirements key is the path into the body that clears it.
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.
Operate as the profile
To call any /v3 endpoint as the profile, add the x-profile-id header to a request made with your organization API key:
curl -X POST "https://api.sent.dm/v3/messages" \
-H "x-api-key: $ORG_API_KEY" \
-H "x-profile-id: $PROFILE_ID" \
-H "Content-Type: application/json" \
-d '{
"to": ["+15551234567"],
"template": {"id": "tmpl_123"}
}'The request executes as the profile and the response echoes the scope in an X-Profile-Id header. Only organization API keys can use x-profile-id; profile-scoped keys are rejected with 403, and a profile id outside your organization returns 404. If you would rather hand tenants their own credentials than proxy through your organization key, use the api_key that create returned. See API credentials and isolation.
Listing, updating, and deleting
List with GET /v3/sender-profiles, newest first. page and page_size are both required. Each entry is the same document a single read returns, with one difference: listed markets carry no compliance. Working out what a market owes means a catalog lookup and a document read per market, and doing that for every profile on a page would turn one request into dozens to answer a question you did not ask. Read a single profile for that.
compliance being absent is not the same as it being empty. Absent means it was not worked out here; empty means the market owes nothing.
Update with PATCH /v3/sender-profiles/{id}, which changes name, short_name, and description, and only those. A profile's identity, its markets, and its compliance each have a separate sub-resource, because each has its own validation and external consequences: a brand change may need re-registering, a market change provisions a sender. Omitting a field leaves it alone and sending null clears it, so {} is refused rather than returning a 200 and an unchanged profile. name and short_name cannot be cleared.
Delete with DELETE /v3/sender-profiles/{id}, which returns 204. The delete is soft: the profile stops being usable and stops appearing in reads, and its message history is kept, because deleting a profile must not delete the record of what it sent. Anything the profile still held goes with it: a phone number returns to our inventory and can be given to whoever asks next, and a dedicated WhatsApp account is deregistered from Meta. A profile that only inherits holds nothing dedicated, so it releases nothing.
A released number does not stay reserved for you, and getting the same number back afterwards is not something we can promise.
Migrating from /v3/profiles
The /v3/profiles endpoints and the profile-campaign endpoints under them are deprecated. They still work, so nothing breaks today, but new integrations should use /v3/sender-profiles.
| Old | New |
|---|---|
POST /v3/profiles then POST /v3/profiles/{profileId}/complete with a webHookUrl | POST /v3/sender-profiles: one call, no completion step and no callback to receive |
POST /v3/profiles/{profileId}/campaigns | compliance.campaign on the market call, per market |
brand object on create | compliance.brand, alongside the campaign it is filed against |
GET /v3/profiles/{profileId} for a single flattened status | GET /v3/sender-profiles/{id}, with a status per channel and per market |
Six fields on the old endpoints are now accepted and ignored, and are worth checking for in existing code because a request carrying them still succeeds:
| Field | Behaviour now |
|---|---|
inherit_contacts, inherit_templates | Accepted and ignored. Contact and template inheritance between profiles is gone: a profile sees only its own records, while the organization sees those of every profile beneath it. Every profile reads back false. |
allow_contact_sharing, allow_template_sharing | Accepted and ignored, reading back false, for the same reason. |
sending_phone_number_profile_id, sending_whatsapp_number_profile_id | Accepted and ignored. A profile can no longer be pointed at another profile's number; give it a dedicated sender with channels.sms instead. Both read back null on /v3/profiles responses. Where the field is populated on another read, such as GET /v3/me, it reports which account holds the number in inventory. It is never a sender you can choose. |
These were bound rather than removed so an SDK that assigns them keeps compiling. Nothing is refused, because a 400 would break an integration that is otherwise working. Even so, a caller that reads back what it wrote can see it did not take.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
400 "short_name is required" | short_name was omitted | Send short_name as well as name |
400 "short_name must be 3–11 characters…" | The sender id is malformed | Use 3–11 characters of letters, numbers, and spaces with at least one letter |
400 "compliance.brand is required when channels.sms.number_type is TEN_DLC" | A 10DLC market with no identity to file against | Send compliance.brand as {"inherit": true} or with the identity's fields |
400 "channels.sms.number_type is required" | channels.sms supplied with only a country | Add number_type |
400 "billing.fallback cannot be set while billing.inherit is true" | Both set | Drop fallback; inheriting already bills the organization |
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 |
403 "You do not have admin access to this organization" | Your API key's user lacks the admin role | Ask the organization owner to grant it (see roles and permissions) |
403 "Profile API keys cannot use x-profile-id" | Scoping header sent with a profile-scoped key | Use an organization API key, or drop the header |
404 on a profile you believe exists | The profile belongs to another organization | Confirm the id; a foreign id is 404 by design |
501 from the requirements endpoint | channel=whatsapp or channel=rcs | Only sms is answered today |
A write to sending_phone_number_profile_id reads back null | The field is reported, not set | Give the profile a dedicated sender with channels.sms |
Related pages
- Sender Profiles - which resources are dedicated and which are inherited
- Managing channels - adding SMS markets, WhatsApp, and RCS to a profile
- Creating a Sender Profile - the dashboard flow
- Multi-Tenant Architectures - choosing shared versus per-tenant setups
- 10DLC Registration - registering for US SMS
- Refer to the create, list, read, update, and delete Sender Profile references for every field and response
Multi-tenant architectures on Sent: profiles vs. accounts
Choose a tenant model for your messaging platform: Sender Profiles with per-tenant provisioning, one shared Sent account, or separate accounts per tenant.
Help & Reference Overview
Quick answers when you need help with Sent: frequently asked questions, problem-solution troubleshooting guides, error code references, and support channels.