Create and activate sub-account profiles via the API

This guide shows you how to provision a sub-account Sender Profile entirely through the API: create the profile, run the asynchronous completion step, handle the webhook callback, and confirm the profile is ready to operate. It is written for platforms and resellers that onboard tenants programmatically; for the dashboard flow, see Creating a Sender Profile, for the underlying concepts, see Sender Profiles, and for choosing between shared and per-tenant architectures, see Multi-Tenant Architectures.

Before you start, you need:

  • An organization account. Call GET /v3/me: the response type must be organization.
  • An organization API key whose user has the admin role in the organization. See roles and permissions for how roles are granted.
  • A WhatsApp Business Account: either your organization has completed WhatsApp Embedded Signup, or you have direct credentials (waba_id, phone_number_id, access_token) from a Meta Business Manager System User with whatsapp_business_messaging and whatsapp_business_management permissions.

All requests authenticate with the x-api-key header.

Provision and activate the profile

Choose inheritance, sharing, and billing settings

Decide these before creating the profile: they determine which resources the sub-account shares with your organization and who pays for its messaging.

Inheritance and sharing flags:

FieldDefaultEffect
inherit_contactstrueProfile reads the organization's contacts
inherit_templatestrueProfile reads the organization's templates
inherit_tcr_brandtrueProfile uses the organization's TCR brand registration
inherit_tcr_campaigntrueProfile uses the organization's TCR campaign
allow_contact_sharingfalseProfile's own contacts become visible to other profiles
allow_template_sharingfalseProfile's own templates become visible to other profiles

If your tenants must not see each other's data, set inherit_contacts and inherit_templates to false and leave the sharing flags off. If your tenants are brands that need their own US A2P registration, set inherit_tcr_brand and inherit_tcr_campaign to false and supply a brand object in the create request (next step). To run all tenants under your own registration, keep the defaults.

Billing model (billing_model, default profile):

ValueWho is billedRequirements
organizationThe organization's billing details are usedNo profile-level billing info
profileThe profile is billed independentlybilling_contact required
profile_and_organizationProfile billed first, organization as fallbackbilling_contact required

Use organization when your platform absorbs messaging costs and invoices tenants itself. Use profile when each tenant pays Sent directly. Use profile_and_organization when tenants pay directly but you guarantee their usage. With profile or profile_and_organization you may also pass payment_details (card number, MM/YY expiry, CVC, ZIP code); card details are never stored on Sent's servers and are forwarded directly to the payment processor. Passing payment_details with billing_model: "organization" is rejected.

Create the profile

Send POST /v3/profiles. Only name is required to create, but set short_name (3–11 characters: letters, numbers, and spaces, with at least one letter) and description now: the completion step in this guide rejects profiles that lack them.

For a tenant with its own TCR brand and direct billing:

curl -X POST "https://api.sent.dm/v3/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",
    "description": "Sender profile for tenant 42",
    "inherit_contacts": false,
    "inherit_templates": false,
    "inherit_tcr_campaign": false,
    "billing_model": "profile",
    "billing_contact": {
      "name": "Tenant 42 Coffee Co",
      "email": "billing@tenant42.example.com",
      "phone": "+12025551234",
      "address": "123 Main Street, New York, NY 10001, US"
    },
    "brand": {
      "contact": {
        "name": "Jane Doe",
        "businessName": "Tenant 42 Coffee Co",
        "email": "jane@tenant42.example.com"
      },
      "business": {
        "legalName": "Tenant 42 Coffee Company LLC",
        "country": "US"
      },
      "compliance": {
        "vertical": "PROFESSIONAL",
        "brandRelationship": "SMALL_ACCOUNT",
        "isTcrApplication": true
      }
    }
  }'

Providing brand implicitly sets inherit_tcr_brand to false; it cannot be combined with inherit_tcr_brand: true. If you keep the organization's registration instead, omit brand and the inheritance flags, but read the callout in the next step first.

If the profile needs its own WhatsApp Business Account, add direct credentials to the request body:

"whatsapp_business_account": {
  "waba_id": "123456789012345",
  "phone_number_id": "987654321098765",
  "access_token": "EAAxxxxxxxxxxxxxxx"
}

Omit the field to inherit the organization's WhatsApp Business Account instead. If you omit it and the organization has not completed WhatsApp Embedded Signup, the request fails with HTTP 422.

A successful request returns 201 with the profile in data, including its id (every later call needs it) and "status": "incomplete". The Idempotency-Key header makes retries safe; responses are cached for 24 hours per key. Refer to the create profile reference for the full field list.

Start completion with a webhook URL

Completion validates the profile, connects it to the SMS and WhatsApp providers, and sets its final status. It runs in the background, so the request requires a webHookUrl that Sent calls when processing finishes, on success or failure:

curl -X POST "https://api.sent.dm/v3/profiles/$PROFILE_ID/complete" \
  -H "x-api-key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "webHookUrl": "https://your-app.example.com/webhooks/profile-complete"
  }'

A 202 response ("Profile completion in progress") means validation passed and processing started. A 200 means the profile was already completed; the body contains its current status. A 400 means a prerequisite failed: the profile needs name, short_name, and description set, its own KYC submission, an available TCR brand (its own or the organization's), and, for TCR applications, at least one campaign, either inherited or created with POST /v3/profiles/{profileId}/campaigns.

Completion requires the profile itself to have brand (KYC) information on file, even when inherit_tcr_brand is true. When provisioning entirely through the API, include the brand object in the create request. A profile that inherits the TCR brand cannot receive a brand object through the API, so its KYC information must be submitted from the dashboard before completion succeeds.

Handle the completion callback

When the background process finishes, Sent sends a POST request with a JSON body to your webHookUrl:

{
  "profileId": "770e8400-e29b-41d4-a716-446655440002",
  "success": true,
  "status": "COMPLETED",
  "timestamp": "2026-07-25T14:03:11.402Z"
}
FieldMeaning
profileIdUUID of the profile that finished processing
successtrue if provisioning finished, false if it failed
statusCOMPLETED or SUBMITTED on success; failed on failure
timestampWhen the callback was generated (UTC)

COMPLETED means the profile is fully activated. SUBMITTED means processing finished but the profile is not yet sendable. The profile lands there when either the SMS or WhatsApp channel configuration is missing, when a dedicated (non-inherited) TCR brand or campaign has not yet been submitted to TCR, or when a non-TCR profile declares a main destination country, which requires more information before activation.

A minimal receiver:

app.post("/webhooks/profile-complete", (req, res) => {
  res.sendStatus(200);

  const { profileId, success, status } = req.body;
  if (success) {
    // status is "COMPLETED" (ready) or "SUBMITTED" (pending registration/config)
    markTenantProvisioned(profileId, status);
  } else {
    // status is "failed", retry the complete call or investigate
    flagTenantProvisioningFailure(profileId);
  }
});

Sent calls the webhook once and does not retry. If your endpoint misses the callback, poll the profile status (next step) instead.

Verify the profile

Confirm the outcome with GET /v3/profiles/{profileId}:

curl "https://api.sent.dm/v3/profiles/$PROFILE_ID" \
  -H "x-api-key: $ORG_API_KEY"

The status field reports the public setup state: approved corresponds to the COMPLETED webhook status, submitted to SUBMITTED, processing means completion is still running, and failed means it did not finish. To see the profile in context, call GET /v3/me with your organization key: the profiles array lists every child profile with its status and the calling user's role in it.

Operate on the child profile

To call any /v3 endpoint as the new 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 (its templates, contacts, numbers, and settings apply) 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 prefer to hand tenants their own credentials instead of proxying through your organization key, each profile also has its own API key. See API credentials and isolation.

Reuse sender numbers across profiles

If a new profile should send from a number already provisioned on another profile, reference that profile instead of provisioning a new number. Update the profile with PATCH /v3/profiles/{profileId}:

  • sending_phone_number_profile_id: use another profile's SMS number and provider configuration. Completion then copies that configuration instead of provisioning a new SMS setup.
  • sending_whatsapp_number_profile_id: use another profile's WhatsApp number configuration.
curl -X PATCH "https://api.sent.dm/v3/profiles/$PROFILE_ID" \
  -H "x-api-key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sending_phone_number_profile_id": "660e8400-e29b-41d4-a716-446655440000"
  }'

When a profile inherits the organization's WhatsApp Business Account at creation, sending_whatsapp_number_profile_id is set to the organization automatically. Set the referenced profile IDs before calling the completion endpoint so the copied configuration counts toward the channel checks that decide COMPLETED versus SUBMITTED. Refer to the update profile reference for all updatable fields, including allow_number_change_during_onboarding.

Troubleshooting

SymptomLikely causeFix
403 "You do not have admin access to this organization" on createYour API key's user lacks the admin roleAsk the organization owner to grant the admin role (see roles and permissions)
422 "Organization does not have a WhatsApp Business Account configured"whatsapp_business_account omitted and no Embedded Signup completedComplete Embedded Signup for the organization, or pass direct WABA credentials
400 "Profile ShortName is required" or "Profile Description is required" on completeProfile was created with only namePATCH the missing fields, then retry
400 "KYC form submission is required before completing profile"The profile has no brand (KYC) submission of its ownInclude brand in the create request, or submit KYC from the dashboard for inheriting profiles
400 "TCR applications must have at least one campaign before completing profile"Dedicated TCR registration without a campaignCreate a campaign for the profile's brand, or set inherit_tcr_campaign: true
400 "Missing required compliance documents for the following countries: …"A main destination country requires documentsUpload the listed documents in the dashboard, then retry
403 "Profile API keys cannot use x-profile-id"Scoping header sent with a profile-scoped keyUse an organization API key, or drop the header
Webhook arrives with success: falseBackground provisioning failedRetry the complete call; if it keeps failing, contact support with the X-Request-Id response header

On this page