---
title: "Create a sender profile"
method: POST
path: "/v3/sender-profiles"
tags: ["Sender Profiles"]
---

# Create a sender profile

`POST /v3/sender-profiles`

## Request

- Base URL: `https://api.sent.dm`
- URL: `https://api.sent.dm/v3/sender-profiles`
- Auth: one of:
  - API key in header `x-api-key`
  - HTTP bearer

Creates a sender profile under your organization. A profile is a customer of yours: it sends under its own or your identity, and your customers never see Sent.

## The minimum

`{ "name": "Example Retail", "short_name": "Example" }`. `short_name` is the SMS sender id and is required. Inbox and opt-out list come from your organization. Its brand and campaign are your organization's only where `compliance` says `{ "inherit": true }` — omitted, the profile has neither. Billing defaults to the profile's own balance, with your organization covering what it cannot.

**A sender is not one of them.** The brand and the campaign can be inherited; a number cannot. A profile either has a sender of its own — `channels.sms` — or it has none and terminates through the shared routes for that country.

## Saying what a profile owns

Every capability is one of three things, and there is no flag per state:

- **Its own** — supply the capability's fields. Data is what owning looks like.
- **The organization's** — `{ "inherit": true }`, with nothing else.
- **None** — omit the block.

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. A customer has exactly one brand, so `brand` resolves without one.

**The campaign has no block of its own.** It is only ever a registration, and every field of one is something a specific market demands — so it lives in `compliance`, alongside the rest of what that market asks for, and it says whose it is there too: `"compliance": { "campaign": { "inherit": true } }` runs the profile on your organization's campaign and brand, and stops this call asking for any campaign field. `"brand": { "inherit": true }` alone inherits the brand only: the campaign is then the profile's own. Inheriting takes *all* of your organization's, because inheritance resolves an owner rather than a single row.

A profile with a `brand` of its own **cannot** inherit the campaign, and asking is refused with `400`: a campaign is filed against a brand registration, and your organization's campaign is filed against your organization's brand rather than this profile's. Inherit both, own both, or inherit the brand and own the campaign.

`channels.sms` is the market the profile starts in — one `country` and `number_type`. It decides what else this call asks for: the compliance catalog is keyed on that pair, and it determines which fields `brand` accepts and what `compliance` answers for.

**Several numbers in that market**: `channels.sms.numbers` lists them, one entry per area code, e.g. `[{"area_code": "212", "quantity": 2}, {"quantity": 1}]` for two in 212 and one in any area code. `quantity` defaults to 1, an entry without `area_code` means any area code, and the total is at most 49. The market is created with **one campaign**, the one in `compliance.campaign`, and a US `TEN_DLC` market's numbers are stored on it and assigned when it is approved. A profile inheriting your organization's campaign keeps one number. Every number is listed in `channels.sms[].numbers` with its area code and status.

A profile is a tenant, not a market, so it can go on to send in several — but through `POST /v3/channels/sms` with the `x-profile-id` header, one market and its compliance at a time. Reads return every market as `channels.sms[]`, so the read shape is a list even where this one is not.

`brand` is required when `number_type` is `TEN_DLC` — inherited or its own. A 10DLC registration has nothing to attach to without one, and no later call can supply it, so omitting it is refused with `400` rather than reported as outstanding.

## Use cases and volume

A TCR campaign declares one to five `use_cases`, each with its own `sample_messages` — TCR attributes samples to the use case they illustrate, so they travel together.

How that set registers is derived from `volume` and never declared. Below the low-volume threshold the campaign registers as `LOW_VOLUME` with every entry as a sub-use-case; above it, one entry registers as that use case outright and several register as `MIXED` with sub-use-cases. `MIXED` and `LOW_VOLUME` are therefore refused as `use_case` values: they describe the shape of a set, which is ours to work out.

**Leaving `volume` out is accepted and costs more.** An absent value registers the campaign at the standard tier, which carries the higher fee — so say so when a campaign is low volume. Sole proprietor brands are the exception: their fee is fixed by entity type, and volume only decides the use case.

## Compliance

`brand` is captured identically in every country. Everything a specific market demands on top of it — including every field of the campaign — goes in `compliance`, as key/value entries whose keys depend on the sender's country and type. US `TEN_DLC` registers with The Campaign Registry and asks for a substantial set; an alphanumeric sender may pre-register with documents, or need nothing at all.

You do not have to know any of it. Omit `compliance` and the response lists exactly what is outstanding, with types and accepted values — or call `GET /v3/compliance/requirements` first. Anything Sent supplies on your behalf is never asked for, and is rejected if sent.

Requirements can be conditional, and the condition is published as `required_when` so you can render a form from it rather than discover the rule by being rejected. Every US `TEN_DLC` campaign field carries `compliance.inherit != true` — send `"inherit": true` and none of them is asked for or accepted, because the profile is running on your organization's campaign.

## Uploading documents

Send `application/json` for everything except a market that pre-registers a sender with supporting documents. For those, send `multipart/form-data` instead: put this entire document — unchanged — in a single field named `profile`, and attach each file under a field named after the compliance key it satisfies. Individual form fields per property are not read; the payload is always the one `profile` field.

A pre-registration market has no `brand` and no `campaign` — those are asked for only where a regime registers one — so its `compliance` carries nothing at all, and the documents are the request:

```bash
curl -X POST https://api.sent.dm/v3/sender-profiles \
  -H 'x-api-key: YOUR_SECRET_TOKEN' \
  -F 'profile={
        "name": "Example Retail",
        "short_name": "EXAMPLE",
        "channels": {
          "sms": {
            "country": "XK",
            "number_type": "ALPHANUMERIC",
            "sender_value": "EXAMPLE"
          }
        }
      }' \
  -F 'business_registration=@registration.pdf' \
  -F 'letter_of_authorization=@loa.pdf'
```

`business_registration` and `letter_of_authorization` are illustrative. The real field names are whatever that market configures, so read them from `GET /v3/compliance/requirements`, which lists documents separately from the values — under `attachments`, each with the `field_name` to put the file under.

A document has no JSON form, so naming one in `compliance` is refused with `400` rather than ignored: accepting it would report the requirement as satisfied while no file was ever stored.

## WhatsApp and RCS

Neither can be inherited, so neither takes an `inherit`: the profile's WhatsApp account and RCS agent are its own or it has none. Sharing your organization's would share its inbox and its throughput, which makes the two indistinguishable to a recipient.

`channels.whatsapp` names one of the accounts shared into your organization, and takes the two identifiers nothing else can supply: `waba_id`, because your organization holds one account per client, and `phone_number_id`, because naming the account's number is what assigns it to the profile as the number it sends from.

Everything else is resolved from your organization — the Multi-Partner Solution the account arrived through, the client business portfolio that owns it, the access token and that token's expiry. None of them is accepted here: the first two are Meta's answer about provenance, so a value you sent could assert a partnership Meta disagrees with, and the token is minted per client business and rotates roughly every sixty days.

`phone_number_id` is optional. Omit it to connect the account now and settle the number later — the channel reads as `ACTION_NEEDED` with `reason_code` `CHANNEL_007`, and the profile's WhatsApp messages fail until a number is named. They do not fall back to your organization's account. `POST /v3/channels/whatsapp` connects an account to an existing profile and resolves the same fields the same way.

An account of its own needs a sender of its own, so `channels.whatsapp` requires `channels.sms`. A profile sending SMS on your organization's sender shares that inbox and opt-out list, and is not a separate tenant to give an account to — asking is refused with `400`, before your organization is asked to resolve anything.

`channels.rcs` records the RCS agent. RCS (Rich Communication Services) is the next-generation SMS standard built into Android — it lets you send branded messages with images, buttons and read receipts over the carrier network, with no app install required. Learn more in the [Google RCS documentation](https://developers.google.com/business-communications/rcs-business-messaging).

All fields on the agent form are required except `opt_in_screenshot_url`. The full form must be submitted in one go so the carrier review has everything it needs. A carrier approval no API call can trigger is arranged offline, so the status reports `PROVISIONING` until it is.

## Responses

`201` when the profile is complete on return — the fully-inherited case, where nothing needs registering. `202` when something is still provisioning, with per-capability status and outstanding requirements. `409` when the WhatsApp account supplied is already recorded. `400` covers a wrong value as well as a missing one: a missing brand under `TEN_DLC`, a brand or campaign inherited under `TEN_DLC` that your organization does not have yet, an unknown compliance key, a bad type, a sender id the market does not allow, a `waba_id` that is not one of the accounts shared into your organization, or a WhatsApp account Meta rejects. `502` when Meta could not be reached to verify the WhatsApp account; nothing was created, so retry.

## Headers

- `Idempotency-Key` string

## Request body

- SentDmServicesEndpointsCustomerAPIv3SenderProfilesRequestsCreateSenderProfileRequest: Create a sender profile under the authenticated organization. How ownership is expressed. Every capability is one of two things, and the request says which without a flag per state: { "inherit": true } uses the organization's; supplying the capability's own fields creates one for the profile; omitting the block entirely means the profile has none. Ownership is one boolean — data is what owning looks like, and a second way to say it would let a request contradict itself.
  - `sandbox` boolean: Sandbox flag - when true, the operation is simulated without side effects Useful for testing integrations without actual execution
  - `name` string, required: Display name for the profile. Required.
  - `short_name` string, required: Short name, which is also the SMS sender id — required. 3–11 characters of letters, numbers and spaces, with at least one letter.
  - `description` string, nullable
  - `billing` SentDmServicesCommonSenderProfilesContractsBillingRequest: Who pays for what a sender profile sends. Ownership reads the same way as BrandRequest and CampaignRequest: inherit: true is the organization paying; inherit: false (or an empty block) is the profile paying for itself. An absent block is the one place billing differs from brand and campaign: a profile still has to be paid for, so omitting billing stores ProfileAndOrganizationBilling — the profile pays first and its organization covers what the profile's balance cannot (see ResolveStored). { "name": "..." } stays a complete request. Billing carries no fields of its own yet, so a profile paying for itself is { "inherit": false } — or an empty block, which means the same. That is thinner than brand and campaign, where supplying data is the act of owning; it becomes ordinary the moment billing gains a contact or a payment method to carry.
    - `inherit` boolean: Whether the parent organization pays. false is the profile paying for itself.
  - `notifications` string, nullable: Who is notified about this profile's events: ORGANIZATION, SENDER_PROFILE, or ORGANIZATION_AND_SENDER_PROFILE. Omitting it leaves the profile on the Organization default.
  - `channels` SentDmServicesEndpointsCustomerAPIv3SenderProfilesRequestsSenderProfileChannels: The channels the profile sends on. Each is independently the profile's own or absent — none can be inherited, so none carries a mode and presence is the whole of the request. A profile can send SMS in several markets, but create takes one — the market it starts in. Further markets are added through POST /v3/channels/sms with x-profile-id, and a read returns them all as a list. Create stays singular permanently rather than being a list that accepts one entry: the constraint is in the type, and there is no later widening to break the contract.
    - `sms` SentDmServicesEndpointsCustomerAPIv3SenderProfilesRequestsSenderProfileSms: One SMS market a profile sends in — a country and a sender type. A profile may have several. It is a tenant, not a market: a business sending to two countries is one customer of yours, and forcing it into two profiles would give it two API keys, two inboxes and two contact lists for one business. The platform already assumes many — CustomerChannelProviders is unique on (customer, channel, iso, number). No mode: a sender is the profile's own or it has none. Listing a market is the request for one. Also the discriminator for everything else: the compliance catalog is keyed on (country, number type), and that pair decides which fields brand accepts and what compliance is asked for.
      - `country` string, required: ISO 3166-1 alpha-2 country the sender operates in, e.g. US.
      - `number_type` string, nullable: Sender type: LOCAL, MOBILE, TEN_DLC, TOLL_FREE, SHORT_CODE or ALPHANUMERIC. 10DLC is accepted as a synonym of TEN_DLC. Availability varies by country.
      - `sender_value` string, nullable: The sender's identity, where the caller chooses it rather than the carrier assigning it. Required when number_type is ALPHANUMERIC; omit for carrier-assigned types and one is provisioned.
      - `area_codes` string[], nullable: The area codes to put this sender in, in your own order of preference, e.g. ["415", "628"] for the US or ["20"] for London. Omit it and a number is claimed anywhere in the country. Ordered, and the order is the request. The claim tries each code in turn and stops at the first that yields a number. Nothing sorts the list and a code listed twice is refused. One to fifty entries, each the area code as digits: 415, with the trunk prefix as 020, or with the country's dialing prefix as +1415 (all are stored as the bare code). A dialing prefix from a different country than country is refused. Each code is checked against the country's numbering plan: it must start real local or mobile numbers there, so toll-free and premium ranges, service codes and another country's codes are refused with a reason. LOCAL senders in any country, and US TEN_DLC senders. No other sender type has a geographic area code, and the list is refused with one rather than accepted and dropped. Honoured exactly or not at all. Ask for 415 and you get a 415 number or you get none, never a neighbouring code you did not name. A miss leaves the market with no sender yet and the codes recorded on it, so it is filled when stock appears rather than filled now from somewhere else. Many countries outside North America require address or business documents before a local number can be bought, which can delay or prevent the fill.
      - `numbers` SentDmServicesCommonSenderProfilesContractsSmsNumberRequest[], nullable: The numbers to assign, one entry per area code, e.g. [{"area_code": "212", "quantity": 2}, {"area_code": "415"}, {"quantity": 1}]: two numbers in 212, one in 415 and one in any area code. quantity defaults to 1, and an entry without area_code means any area code. Omit numbers for one number. Only with LOCAL or TEN_DLC, and not together with area_codes. Each area code is validated against country the same way area_codes is and may appear once, and at most one entry may leave area_code out. US TEN_DLC numbers are assigned when the campaign is approved, not at request time. Not available when the profile inherits the organization's campaign.
        - `area_code` string, nullable: The area code, e.g. 212, validated against the request's country. Omit it for numbers in any area code.
        - `quantity` integer, nullable: How many numbers. At least 1; omitted means 1.
    - `whatsapp` SentDmServicesEndpointsCustomerAPIv3SenderProfilesRequestsSenderProfileWhatsapp: The profile's own WhatsApp Business Account, named by the two identifiers the organization cannot answer for you. No mode: a profile's WABA is its own or it has none. Sharing the organization's would mean sharing its inbox and its throughput, which makes the two indistinguishable to a recipient — so inheriting is not offered rather than refused. Everything derivable is derived. Whenever this block is present, the solution the account arrived through, the client business portfolio that owns it, the access token and that token's expiry are all resolved from the organization by IPartnerSolutionService.ResolveAccountCredentialsAsync — the first two off the org's shared-account listing, the token minted against the org's own solution. They were previously asked for and are no longer accepted: solution_id and owner_business_id are Meta's answer about provenance, so taking them from the caller let a request assert a partnership Meta disagreed with, and the listing has to be consulted regardless because it is what proves the account is one this organization may reach. No access token, and there never was one here: it is minted per client business portfolio and rotates roughly every sixty days, so a pasted value would be stale before it was useful. Asking a customer to mint a System User token in Meta Business Manager is skilled work in a console they were never meant to see, and it would say nothing about which solution the account came from or when it is revoked.
      - `waba_id` string, nullable: WhatsApp Business Account id — one of the accounts shared into your organization. Required: the organization holds one per client, and nothing but the caller says which belongs to this profile.
      - `phone_number_id` string, nullable: The Meta phone-number id this profile sends from. Must already be registered under WabaId. Optional. Omit it to attach the account now and settle the number later: the channel reads as ACTION_NEEDED with CHANNEL_007, and the profile's WhatsApp messages fail until a number is named. They do not fall back to the organization's account. Nothing picks one on your behalf.
    - `rcs` SentDmServicesEndpointsCustomerAPIv3SenderProfilesRequestsSenderProfileRcs: Asks for an RCS agent of the profile's own, optionally with the full order form inline. Two modes, distinguished by whether any field is populated: "rcs": {} — intent only. Records that the profile wants its own agent; the order form is submitted separately through POST /v3/channels/rcs once the fields are ready. "rcs": { ...fields... } — full form. All required fields must be present; the agent is created alongside the profile in one call. Omitting opt_in_screenshot_url is allowed.
      - `display_name` string, nullable
      - `description` string, required
      - `agent_use_case` string, nullable
      - `hosting_region` string, nullable
      - `billing_category` string, nullable
      - `brand_name` string, nullable
      - `privacy_policy_url` string, nullable
      - `terms_and_conditions_url` string, nullable
      - `website_url` string, nullable
      - `brand_color` string, nullable
      - `logo_url` string, nullable
      - `banner_url` string, nullable
      - `brand_phone_number` string, nullable
      - `customer_support_phone_number` string, nullable
      - `brand_email` string, nullable
      - `customer_support_email` string, nullable
      - `contact_name_and_title` string, nullable
      - `company_ein` string, nullable
      - `entity_type` string, nullable
      - `official_address` SentDmServicesCommonChannelsContractsRcsAddress: The registered address of the company behind the agent.
        - `street` string, nullable
        - `city` string, nullable
        - `state` string, nullable
        - `postal_code` string, nullable
        - `country` string, nullable
      - `brief_company_description` string, nullable
      - `opt_in_process_description` string, nullable
      - `opt_in_screenshot_url` string, nullable: Optional — uploaded separately and its URL may not be known at submission time.
      - `start_message` string, nullable
      - `help_message` string, nullable
      - `stop_message` string, nullable
      - `sample_messages` string[], nullable
  - `compliance` MarketCompliance: The compliance values a market registers with. `brand` and `campaign` apply to US `TEN_DLC`; other markets fill this map with the document keys their own requirements name.
    - `brand` object: Business identity. The identity the registration is filed against. Omit to have none; { "inherit": true } to register under your organization's.
      - `inherit` boolean: Run on the organization's brand. True to register under the parent organization's identity rather than one of this profile's own; every field below is then neither asked for nor accepted. Omitted, the brand is the organization's unless this block supplies identity fields, which says plainly enough that it wants one of its own.
      - `legal_name` string: Registered legal name. TCR companyName — the name on the incorporation documents, which is often not the name recipients would recognise. Required when `compliance.brand.inherit != true`.
      - `business_name` string: Public-facing name. TCR displayName — the name recipients would recognise. Named for what it holds rather than for TCR's field, which is why it is not `display_name`. Required when `compliance.brand.inherit != true`.
      - `tax_id` string: Tax identifier. TCR ein. TcrBrandSubmissionValidator refuses a submission without one, so it is wanted here rather than discovered at filing time. Required when `compliance.brand.inherit != true`.
      - `tax_id_type` string: Kind of tax identifier. Which kind of identifier `tax_id` is, e.g. us_ein. Derived onto the brand form, and not itself sent to TCR. Required when `compliance.brand.inherit != true`.
      - `ein_issuing_country` string, iso3166_alpha2: Country that issued the tax id. TCR einIssuingCountry — where the tax id was issued, which is not always where the business trades. Defaults to `country` when omitted, which is right for most. Takes the code or the country spelled out, as `country` does.
      - `entity_type` 'PRIVATE_PROFIT' | 'PUBLIC_PROFIT' | 'NON_PROFIT' | 'SOLE_PROPRIETOR' | 'GOVERNMENT': Entity type. TCR entityType. Decides which vetting TCR applies and, for SOLE_PROPRIETOR, fixes the campaign fee regardless of volume. Also refused at submission if absent. Required when `compliance.brand.inherit != true`.
      - `street` string: Street address. Required when `compliance.brand.inherit != true`.
      - `city` string: City. Required when `compliance.brand.inherit != true`.
      - `state` string: State or region. TCR state. Two letters for a US registration. Required when `compliance.brand.inherit != true`.
      - `postal_code` string: Postal code. Required when `compliance.brand.inherit != true`.
      - `country` string, iso3166_alpha2: Country of registration. Two letters — US. The country spelled out in full is accepted too and stored as its code, which is what a read answers with and what is filed. Required when `compliance.brand.inherit != true`.
      - `website` string, url: Website. TCR website. Optional, and worth sending: carriers weigh it when vetting, so a brand that has one and omits it is vetted on less than it could be.
      - `contact_name` string: Responsible contact. Split into TCR's firstName/lastName on submission. BrandService refuses a blank one, so a missing name is reported here rather than thrown after the profile was written. Required when `compliance.brand.inherit != true`.
      - `contact_email` string, email: Contact email. TCR email. Required when `compliance.brand.inherit != true`.
      - `contact_phone` string, e164: Contact phone. TCR sends it as both phone and mobilePhone, so it is asked for once. Named for the contact it belongs to rather than as a bare `phone` on a block that also carries an address. Required when `compliance.brand.inherit != true`.
    - `campaign` object: Messaging campaign. What the programme sends and how recipients consented. Omit to run on your organization's; supplying it takes one of your own. { "inherit": true } says so outright. Required when `compliance.campaign.inherit != true`.
      - `inherit` boolean: Run on the organization's campaign. True to register under the parent organization's programme rather than one of this profile's own; every field below is then neither asked for nor accepted. Owning the identity and inheriting the campaign is refused — a campaign is filed against an identity registration, and chk_tcr_inheritance forbids that pairing outright.
      - `description` string: What this programme sends. TCR description: what recipients will get and why, in a sentence. The only one of these TCR takes verbatim as a description of the programme, which is why it is asked for rather than derived from the customer's name. Required when `compliance.campaign.inherit != true`.
      - `message_flow` string: Consent narrative. TCR messageFlow: how recipients opt in, 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. Required when `compliance.campaign.inherit != true`.
      - `use_cases` object[]: Use cases. What the campaign sends, and an example of each. One to five entries; how they register is decided by volume, never declared. Required when `compliance.campaign.inherit != true`.
        - `use_case` 'MARKETING' | 'ACCOUNT_NOTIFICATION' | 'CUSTOMER_CARE' | 'FRAUD_ALERT' | '2FA' | 'DELIVERY_NOTIFICATION' | 'SECURITY_ALERT' | 'M2M' | 'HIGHER_EDUCATION' | 'POLLING_VOTING' | 'PUBLIC_SERVICE_ANNOUNCEMENT', required: Use case. What this entry sends. MIXED and LOW_VOLUME are refused — volume decides those.
        - `sample_messages` unknown[], required: Sample messages. TCR sample1..sample5 for this use case. One to five, each carrying the brand name and an opt-out instruction.
          - unknown
      - `opt_in_message` string: Opt-in confirmation. TCR optinMessage. Should state message frequency and that rates may apply. Its presence is what sets subscriberOptin. Required when `compliance.campaign.inherit != true`.
      - `opt_out_message` string: Opt-out confirmation. TCR optoutMessage. Its presence is what sets subscriberOptout. Required when `compliance.campaign.inherit != true`.
      - `help_message` string: Help reply. TCR helpMessage. Its presence is what sets subscriberHelp. Required when `compliance.campaign.inherit != true`.
      - `opt_in_keywords` string, csv_keywords: Opt-in keywords. TCR optinKeywords, comma separated, e.g. START,YES. Required when `compliance.campaign.inherit != true`.
      - `opt_out_keywords` string, csv_keywords: Opt-out keywords. TCR optoutKeywords, comma separated. STOP, END, CANCEL, UNSUBSCRIBE and QUIT are honoured regardless of what is declared. Required when `compliance.campaign.inherit != true`.
      - `help_keywords` string, csv_keywords: Help keywords. TCR helpKeywords, comma separated, e.g. HELP,INFO. Required when `compliance.campaign.inherit != true`.
      - `privacy_policy_link` string, url: Privacy policy URL. TCR privacyPolicyLink. Required when `compliance.campaign.inherit != true`.
      - `terms_and_conditions_link` string, url: Terms and conditions URL. TCR termsAndConditionsLink. Required when `compliance.campaign.inherit != true`.
      - `volume` string: Expected monthly volume. Decides how the campaign registers and which fee tier applies: below the low-volume threshold it registers as LOW_VOLUME with sub-use-cases, above it as a single use case or MIXED. Omitting it registers the campaign at the higher-fee standard tier.

## Response `201`

Profile created and complete — nothing needed registering

- SentDmServicesEndpointsCustomerAPIv3SharedApiResponseOfSenderProfileResponse: Standard API response envelope for all v3 endpoints
  - `success` boolean: Indicates whether the request was successful
  - `data` SentDmServicesEndpointsCustomerAPIv3SenderProfilesResponsesSenderProfileResponse: A sender profile as the API reports it — the same document the create call accepts, plus the read-only facts it cannot: each market's status and what it still owes. No status at the profile, brand or campaign level. Brand and campaign are identity, and identity has no lifecycle. What has one is the registration of a sender in a market, and the approval of a channel — so status lives there, where it says which specific thing is blocked rather than flattening several answers into one.
    - `id` string, uuid
    - `organization_id` string, uuid, nullable
    - `name` string
    - `short_name` string, nullable
    - `description` string, nullable
    - `api_key` string, nullable: The API key for this profile. Returned by create and by nothing else. It is shown once; a read that repeated it would turn every profile list into a credential dump and put the key into any log or cache that keeps a response body. Omitted rather than null on a read. A key field that is always present and always empty suggests it could be populated, which is an invitation to populate it; absent says there is nothing to look for. Doing it with a serializer condition rather than a second response type keeps create and read on one shape, which is what stops them drifting.
    - `billing` SentDmServicesEndpointsCustomerAPIv3SenderProfilesResponsesBillingResponse: Who pays for a sender profile's traffic, as a read reports it. Its own type, field for field the same as BillingRequest. Same reason as the brand: a response is not a request, whatever it carries.
      - `inherit` boolean: Whether the organization pays, rather than the profile.
    - `notifications` string, nullable: Who is notified about this profile's events: ORGANIZATION, SENDER_PROFILE, or ORGANIZATION_AND_SENDER_PROFILE. Always present — a profile that never chose reports as ORGANIZATION, the value a create would send to get the default behaviour.
    - `channels` SentDmServicesCommonChannelsContractsSenderProfileChannelsState: The channels a profile sends on. SMS is a list because a profile can send in several markets; WhatsApp and RCS are single, because a profile has one business account and one agent.
      - `customer_id` string, uuid, nullable: Which customer these channels belong to — the key's own, or the profile named in x-profile-id. Same reason the brand carries one: as the response to /v3/channels there is no other id in the document, and under x-profile-id this is the profile the call resolved to rather than the caller. Absent when nested inside a sender profile, whose own id already says whose they are.
      - `sms` SentDmServicesCommonChannelsContractsSmsMarketState[]
        - `country` string, nullable
        - `number_type` string, nullable
        - `sender_value` string, nullable: The sender itself. Null until a carrier-assigned number is procured. For a market holding several numbers, the first active one (the first assigned while none is active) — every number is in numbers.
        - `numbers` SentDmServicesCommonChannelsContractsSmsNumberState[]: Every number this market holds or is still owed, each with its area code and its own status (ACTIVE once it can send, PROVISIONING while it is still to be assigned, INACTIVE once it could send and no longer can). One entry for a market holding one number. US 10DLC numbers are assigned when the campaign is approved, so they read as owed until then. Read-only.
          - `sender_value` string, nullable: The number, or null while it is still to be assigned.
          - `area_code` string, nullable: The number's area code, e.g. 212. While the number is still to be assigned, the area code it was asked for, or null when it was asked for in any area code.
          - `status` string, nullable: ACTIVE once the number can send; PROVISIONING while it is still to be assigned; INACTIVE once it could send and no longer can.
        - `status` 'PROVISIONING' | 'ACTION_NEEDED' | 'PENDING_REVIEW' | 'ACTIVE' | 'INACTIVE', nullable: Where this market's sender stands, in terms of who is holding it up — PROVISIONING, ACTION_NEEDED, PENDING_REVIEW or ACTIVE. Read-only. Resolved rather than reported raw: a live sender reads ACTIVE whatever it still owes, a market with outstanding requirements or an open resubmission request reads ACTION_NEEDED, and a market that registers with a regime reads PENDING_REVIEW once its brand exists — filing to The Campaign Registry is ours to do and need not have happened yet, so a dedicated brand created through this API reads PENDING_REVIEW from the moment it is stored. Describes whether the market can send, not each number: a market holding several numbers reads ACTIVE as soon as one of them is, whatever the others are doing. Each number's own status is in numbers.
        - `reason_code` string, nullable: Why the market is not ACTIVE, as a stable code: an ErrorCodes CHANNEL_xxx value such as CHANNEL_001 (something you owe) or CHANNEL_009 (under review). Switch on this rather than on reason. Omitted while ACTIVE. Read-only; when nothing more specific is known it is the generic reason for the status.
        - `reason` string, nullable: A sentence for reason_code: this market's own explanation, naming what is outstanding where that is you. Omitted while ACTIVE.
        - `compliance` SentDmServicesCommonChannelsContractsSuppliedComplianceState: What a market has been given: the identity it registers under, its programme, and any documents attached. What it does not carry is what the market asks for. That is the subject of GET /v3/compliance/requirements, and it is the same answer for every caller — a description of what a compliance regime wants, not a record of one customer's progress through it. It was reported here as well for a while, which put the same array in six response shapes and left a caller deciding which of two sources to believe. Present on a list read for markets that register (carrying brand and campaign), but with documents absent — documents are not fetched for a list, because a catalog lookup and a document read per market would multiply across a page. Absent documents is distinct from an empty list: absent says they were not fetched; empty says the market has been given none. The parent object is null only when the market registers with nobody and compliance was not computed — nothing to show at all.
          - `brand` object, nullable: The identity this market registers under, with inherit saying whose it is. Reported here rather than on the profile because it belongs to the registration this market files, and only one market files one. It was a top-level block for a while, which put a per-registration value beside a list of markets and left a caller to work out which market it belonged to. Absent for a market that registers with nobody — such a market asks for no identity, so there is none to report. Absent and null mean different things: absent says this market does not ask, null would say it asks and nothing was supplied. Untyped, like the request side, because its members are declared by the market's own schema rather than by a C# class. A typed pair here would be a second definition of what a market wants, free to drift from the one that validates.
          - `campaign` object, nullable: The programme this market registers, with inherit saying whose it is. One, not a list. TcrCampaigns permits several and an account built on the admin side may hold them, but this surface offers one — which is what lets the market's PATCH be an upsert rather than a collection with an addressable create behind it. An account holding several is reported as its first and refused on write, rather than half-edited. Carries no id. Nothing addresses a campaign, and an undeclared key would be refused if the caller sent this object back — which it is meant to be able to do.
          - `documents` SentDmServicesCommonChannelsContractsSuppliedDocument[], nullable: What has been supplied for this market. Files, not values — the declared halves above carry the values. A document cannot be a JSON value, so it is sent as multipart on the channel call and reported here as a reference. Absent on a list read, which fetches identity but does not compute compliance documents per market. Absent and empty mean different things: absent says the documents were not fetched; empty says the market has been given none.
            - `key` string: The catalog's name for this document, matching the requirement it satisfies.
            - `file_name` string, nullable
            - `document_id` string, uuid, nullable: Identifier of the upload, for fetching it back through the documents endpoints.
      - `whatsapp` SentDmServicesCommonChannelsContractsWhatsappChannelState: The profile's WhatsApp account. Its status is Meta's verification state.
        - `waba_id` string, nullable
        - `phone_number_id` string, nullable: The Meta phone-number id the profile sends from, or null when the account is connected but no number has been named yet — which is what a provisioning status means here.
        - `solution_id` string, nullable: Derived, not echoed. The request no longer accepts this — it is read off the organization's shared-account listing at connect time and reported back so a caller can see which partnership the account actually arrived through.
        - `owner_business_id` string, nullable: Derived from the organization's shared-account listing, like SolutionId.
        - `status` 'PROVISIONING' | 'ACTION_NEEDED' | 'PENDING_REVIEW' | 'ACTIVE' | 'INACTIVE', nullable: Read-only. Never carries a token — the row holds that and it is not returned. ACTION_NEEDED when no account is connected or the account has no phone number: both are yours to do.
        - `reason_code` string, nullable: Why the account is not ACTIVE, as a stable code: an ErrorCodes CHANNEL_xxx value such as CHANNEL_001 (something you owe) or CHANNEL_009 (under review). Switch on this rather than on reason. Omitted while ACTIVE. Read-only; when nothing more specific is known it is the generic reason for the status.
        - `reason` string, nullable: A sentence for reason_code, saying what to do where it is yours. Omitted while ACTIVE.
      - `rcs` SentDmServicesCommonChannelsContractsRcsAgentState: An RCS agent as its owner sees it: the order form back, plus where the submission stands. Not RcsSenderResponse. That one is the admin projection and carries the pipeline we track about a customer's request — internalStatus, statusReason, isLaunched — and the S3 keys behind the assets. None of that is the customer's to read: an internal status is a note to ourselves, and an S3 key is a storage detail that would become a contract the moment it was published. What is here is what the caller sent, in the names it sent them under, plus the three things only we can answer: the record's id, its status, and the number it sends from. So a read after a write shows the form as stored rather than as echoed.
        - `id` string, uuid, nullable: The agent record. Stable across the whole approval, and what support will ask for. Nullable and omitted when absent: a sender profile with RCS intent records the channel but has no agent until the order form is submitted, so no id exists to report. A zero GUID is not a valid id and is worse than the field being absent.
        - `status` 'PROVISIONING' | 'ACTION_NEEDED' | 'PENDING_REVIEW' | 'ACTIVE' | 'INACTIVE', nullable: Where the agent stands, in terms of who is holding it up — PROVISIONING, ACTION_NEEDED, PENDING_REVIEW or ACTIVE. Resolved from both of the provider's axes rather than reported raw: an agent whose resubmission we have asked for reads ACTION_NEEDED however its carrier status still reads, and an agent not yet handed over reads PROVISIONING rather than pending a review nobody has started.
        - `reason_code` string, nullable: Why the agent is not ACTIVE, as a stable code: an ErrorCodes CHANNEL_xxx value such as CHANNEL_001 (something you owe) or CHANNEL_009 (under review). Switch on this rather than on reason. Omitted while ACTIVE. Read-only; when nothing more specific is known it is the generic reason for the status.
        - `reason` string, nullable: A sentence for reason_code, saying what to do where it is yours. Omitted while ACTIVE.
        - `phone_number` string, nullable: The number the agent sends from, derived from brand_phone_number.
        - `display_name` string, nullable
        - `description` string, nullable
        - `agent_use_case` string, nullable
        - `brand_name` string, nullable
        - `privacy_policy_url` string, nullable
        - `terms_and_conditions_url` string, nullable
        - `website_url` string, nullable
        - `brand_color` string, nullable
        - `brand_phone_number` string, nullable
        - `customer_support_phone_number` string, nullable
        - `brand_email` string, nullable
        - `customer_support_email` string, nullable
        - `contact_name_and_title` string, nullable
        - `company_ein` string, nullable
        - `entity_type` string, nullable
        - `official_address` SentDmServicesCommonChannelsContractsRcsAddress: The registered address of the company behind the agent.
          - `street` string, nullable
          - `city` string, nullable
          - `state` string, nullable
          - `postal_code` string, nullable
          - `country` string, nullable
        - `brief_company_description` string, nullable
        - `opt_in_process_description` string, nullable
        - `start_message` string, nullable
        - `help_message` string, nullable
        - `stop_message` string, nullable
        - `sample_messages` string[], nullable
        - `created_at` string, date-time, nullable: Omitted when no real agent record exists — the intent-only path has no creation timestamp.
      - `mms` SentDmServicesCommonChannelsContractsMmsMarketState[]: The markets this profile can send MMS in, one entry per country, or empty when none. Appears alongside sms rather than replacing an entry in it: the two are separate senders on the same number and a caller needs to see both — a market can be live for SMS while its MMS row is still provisioning, or MMS-incapable entirely because the market's sender-type SKU does not carry it.
        - `country` string, nullable: ISO-3166 alpha-2 of the market.
        - `number_type` string, nullable: The sender type the market sends from, mirroring the SMS sender.
        - `sender_value` string, nullable: The sending number. Null while the market is still being provisioned.
        - `status` string, nullable: Where this market's MMS sender stands. Read-only.
      - `voice` SentDmServicesCommonChannelsContractsVoiceNumberState[]: The numbers this profile carries phone calls on, one entry per number, or empty when none. Voice is per number like an SMS market is per country: each number is its own channel row, turned on and off on its own, and one of them is the line app-originated calls fall back to.
        - `number` string: The number, in E.164.
        - `status` string: ACTIVE while the number carries calls, INACTIVE once it was turned off. Nothing provisions: a number the customer holds can carry calls the moment voice is turned on for it.
        - `default_for_app_calls` boolean: Whether this is the line app-originated calls are placed from when a voice token names no number. Exactly one active voice number carries it while the profile has any.
        - `callback_url` string, nullable: Where Sent asks what to do with each call on this number: a signed question is POSTed here when a call arrives or a caller presses a key, and the answer decides the call. The signing secret is not on this read; it is shown when voice is turned on and by the rotate endpoint.
        - `agent_id` string, nullable: The voice agent calls to this number go to, such as agent_9f2ab000-0000-4000-8000-000000000001. It wins over callback_url for calls to the number; calls your app places from the number still follow your app. Null when no agent answers the number.
        - `routes_to` string: Who answers calls to this number: agent, the voice agent in agent_id; agent_pending, that agent once the number has finished moving to it, with calls possibly unanswered until then; callback, your callback_url; or none, in which case calls to it are declined.
        - `created_at` string, date-time
        - `updated_at` string, date-time
    - `compliance` SentDmServicesCommonChannelsContractsSuppliedComplianceState: What a market has been given: the identity it registers under, its programme, and any documents attached. What it does not carry is what the market asks for. That is the subject of GET /v3/compliance/requirements, and it is the same answer for every caller — a description of what a compliance regime wants, not a record of one customer's progress through it. It was reported here as well for a while, which put the same array in six response shapes and left a caller deciding which of two sources to believe. Present on a list read for markets that register (carrying brand and campaign), but with documents absent — documents are not fetched for a list, because a catalog lookup and a document read per market would multiply across a page. Absent documents is distinct from an empty list: absent says they were not fetched; empty says the market has been given none. The parent object is null only when the market registers with nobody and compliance was not computed — nothing to show at all.
      - `brand` object, nullable: The identity this market registers under, with inherit saying whose it is. Reported here rather than on the profile because it belongs to the registration this market files, and only one market files one. It was a top-level block for a while, which put a per-registration value beside a list of markets and left a caller to work out which market it belonged to. Absent for a market that registers with nobody — such a market asks for no identity, so there is none to report. Absent and null mean different things: absent says this market does not ask, null would say it asks and nothing was supplied. Untyped, like the request side, because its members are declared by the market's own schema rather than by a C# class. A typed pair here would be a second definition of what a market wants, free to drift from the one that validates.
      - `campaign` object, nullable: The programme this market registers, with inherit saying whose it is. One, not a list. TcrCampaigns permits several and an account built on the admin side may hold them, but this surface offers one — which is what lets the market's PATCH be an upsert rather than a collection with an addressable create behind it. An account holding several is reported as its first and refused on write, rather than half-edited. Carries no id. Nothing addresses a campaign, and an undeclared key would be refused if the caller sent this object back — which it is meant to be able to do.
      - `documents` SentDmServicesCommonChannelsContractsSuppliedDocument[], nullable: What has been supplied for this market. Files, not values — the declared halves above carry the values. A document cannot be a JSON value, so it is sent as multipart on the channel call and reported here as a reference. Absent on a list read, which fetches identity but does not compute compliance documents per market. Absent and empty mean different things: absent says the documents were not fetched; empty says the market has been given none.
        - `key` string: The catalog's name for this document, matching the requirement it satisfies.
        - `file_name` string, nullable
        - `document_id` string, uuid, nullable: Identifier of the upload, for fetching it back through the documents endpoints.
    - `created_at` string, date-time
  - `error` SentDmServicesEndpointsCustomerAPIv3SharedApiError: Error information
    - `code` string: Machine-readable error code (e.g., "RESOURCE_001")
    - `message` string: Human-readable error message
    - `details` object, nullable: Additional validation error details (field-level errors)
    - `doc_url` string, nullable: URL to documentation about this error
  - `meta` SentDmServicesEndpointsCustomerAPIv3SharedApiMeta: Request and response metadata
    - `request_id` string: Unique identifier for this request (for tracing and support)
    - `timestamp` string, date-time: Server timestamp when the response was generated
    - `version` string: API version used for this request

## Other responses

- `202`: Profile accepted; capabilities are provisioning
- `400`: Malformed request
- `401`: Unauthorized - Invalid API credentials
- `403`: A profile API key cannot create profiles
- `409`: The WhatsApp account supplied is already recorded
- `422`: The number type or market asked for cannot be used, or a required compliance document is missing or was rejected
- `500`: Server Error
- `502`: Meta could not be reached to verify the WhatsApp account; nothing was created

## Changes

- **2026-10-28** `41acb2ceb84d`: 4 info
  - added the optional property `data/allOf[#/components/schemas/SentDmServicesEndpointsCustomerAPIv3SenderProfilesResponsesSenderProfileResponse]/channels/allOf[#/components/schemas/SentDmServicesCommonChannelsContractsSenderProfileChannelsState]/voice/items/agent_id` to the response with the `201` status
  - added the optional property `data/allOf[#/components/schemas/SentDmServicesEndpointsCustomerAPIv3SenderProfilesResponsesSenderProfileResponse]/channels/allOf[#/components/schemas/SentDmServicesCommonChannelsContractsSenderProfileChannelsState]/voice/items/agent_id` to the response with the `202` status
  - added the optional property `data/allOf[#/components/schemas/SentDmServicesEndpointsCustomerAPIv3SenderProfilesResponsesSenderProfileResponse]/channels/allOf[#/components/schemas/SentDmServicesCommonChannelsContractsSenderProfileChannelsState]/voice/items/routes_to` to the response with the `201` status
  - added the optional property `data/allOf[#/components/schemas/SentDmServicesEndpointsCustomerAPIv3SenderProfilesResponsesSenderProfileResponse]/channels/allOf[#/components/schemas/SentDmServicesCommonChannelsContractsSenderProfileChannelsState]/voice/items/routes_to` to the response with the `202` status
- **2026-10-16** `6c829543a93e`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/sentdm/apis/sent-dm-api-v3/changes/v3/sender-profiles/post.md)

---

[API](https://skmtc.dev/sentdm/apis/sent-dm-api-v3.md) - [All operations](https://skmtc.dev/sentdm/apis/sent-dm-api-v3/llms.txt) - [OpenAPI document](https://skmtc.dev/sentdm/apis/sent-dm-api-v3/revisions/bdcca958cac7?raw)
