agents/openai.yaml
interface:
display_name: "Sent Profile Provisioning"
short_description: "Run the Sender Profile lifecycle"
default_prompt: "Use $sent-profile-provisioning to plan the API calls that bring a new Sender Profile to a sending state."
references/profile-lifecycle.md
# Sender Profile lifecycle reference
## Table of contents
- [Operation catalog](#operation-catalog)
- [Create field reference](#create-field-reference)
- [Update-only fields](#update-only-fields)
- [Inheritance and sharing matrix](#inheritance-and-sharing-matrix)
- [Billing configuration](#billing-configuration)
- [WhatsApp attachment paths](#whatsapp-attachment-paths)
- [Completion flow and callback](#completion-flow-and-callback)
- [Status vocabulary by surface](#status-vocabulary-by-surface)
- [Error catalog for provisioning](#error-catalog-for-provisioning)
- [Idempotency and sandbox](#idempotency-and-sandbox)
- [Offboarding](#offboarding)
## Operation catalog
Ten operations cover profiles and their campaigns. None is available through MCP, so provisioning is REST-only.
| Operation | Purpose |
| --- | --- |
| `POST /v3/profiles` | Create a profile |
| `GET /v3/profiles` | List profiles |
| `GET /v3/profiles/{profileId}` | Read one profile, including current status |
| `PATCH /v3/profiles/{profileId}` | Update configuration and number references |
| `DELETE /v3/profiles/{profileId}` | Remove a profile |
| `POST /v3/profiles/{profileId}/complete` | Start completion; requires `webHookUrl` |
| `GET /v3/profiles/{profileId}/campaigns` | List campaigns |
| `POST /v3/profiles/{profileId}/campaigns` | Create a campaign |
| `PUT /v3/profiles/{profileId}/campaigns/{campaignId}` | Update a campaign |
| `DELETE /v3/profiles/{profileId}/campaigns/{campaignId}` | Delete a campaign |
Creation requires an organization API key whose owning email holds `admin`. An organization key may target a child with `x-profile-id`; a profile-scoped key sending that header receives `403`, and a profile outside the organization returns `404`.
## Create field reference
| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `name` | string | — | The only required field |
| `icon` | string | — | Display asset |
| `description` | string | — | Free text |
| `short_name` | string | — | 3–11 chars, letters, numbers, spaces, at least one letter |
| `allow_contact_sharing` | boolean | `false` | Exposes this profile's contacts outward |
| `allow_template_sharing` | boolean | `false` | Exposes this profile's templates outward |
| `inherit_contacts` | boolean | `true` | Consumes the organization's contacts |
| `inherit_templates` | boolean | `true` | Consumes the organization's templates |
| `inherit_tcr_brand` | boolean | `true` | Uses the organization's brand; forbids a `brand` object |
| `inherit_tcr_campaign` | boolean | `true` | Inherited campaigns are read-only for this profile |
| `billing_model` | enum | `profile` | `profile`, `organization`, or `profile_and_organization` |
| `billing_contact` | object | — | `name`, `email`, `phone`, `address`; required when the model includes `profile` and none exists |
| `whatsapp_business_account` | object | — | `waba_id` and `access_token` required, `phone_number_id` optional |
| `brand` | object | — | `contact` and `compliance` required, `business` optional; forbidden when `inherit_tcr_brand` is true |
| `payment_details` | object | — | `card_number`, `expiry`, `cvc`, `zip_code`; only for models including `profile` |
| `sandbox` | boolean | `false` | Simulate without side effects |
`payment_details` is ephemeral and forwarded to the payment processor. Never log it, never echo it back to a user, never store it, and never place it in a file that could be committed.
Although creation requires only `name`, completion also requires `short_name`, `description`, profile KYC information, and any required campaign or channel setup. A profile inheriting the organization's TCR brand cannot include a `brand` object in the API request but still needs profile-level KYC submitted through the dashboard before completion.
## Update-only fields
`PATCH /v3/profiles/{profileId}` accepts the create fields plus number references:
- `sending_phone_number_profile_id`
- `sending_whatsapp_number_profile_id`
- `sending_phone_number`
- `whatsapp_phone_number`
- `allow_number_change_during_onboarding`
Model reference identifiers separately from literal numbers, and guard against cycles when one profile's sending number points at another profile that points back.
## Inheritance and sharing matrix
Inheritance pulls resources in; sharing pushes them out. They are independent.
| Configuration | Result |
| --- | --- |
| `inherit_contacts: true`, `allow_contact_sharing: false` | Reads organization contacts; does not expose its own |
| `inherit_contacts: false`, `allow_contact_sharing: true` | Isolated contact store that other profiles may read |
| `inherit_tcr_brand: true`, `inherit_tcr_campaign: true` | Fully inherited compliance posture; campaigns read-only here |
| `inherit_tcr_brand: true`, `inherit_tcr_campaign: false` | Shared legal identity with per-tenant use cases — the common multi-tenant pattern |
| `inherit_tcr_brand: false` | Dedicated brand supplied in the same create request |
For tenant isolation, set `inherit_contacts` and `inherit_templates` to false explicitly, because both default to true and a silently inherited store means one tenant can read another's data model.
## Billing configuration
| Model | Meaning | Requires |
| --- | --- | --- |
| `profile` | The profile pays | `billing_contact`, optionally `payment_details` |
| `organization` | The organization pays | Nothing profile-side |
| `profile_and_organization` | Profile first with organization fallback | `billing_contact` |
Effective balance follows this configuration, so a balance reading for a profile with `organization` billing reflects the organization's funds. Confirm which model a profile uses before interpreting a balance or diagnosing a `BLOCKED` message.
## WhatsApp attachment paths
| Path | How | When |
| --- | --- | --- |
| Organization Embedded Signup | Sent Dashboard only; no public endpoint exists | The organization owns one WABA used across profiles |
| Child inheritance | Omit `whatsapp_business_account` | Tenants share the organization's WABA |
| Dedicated credentials | `whatsapp_business_account` with `waba_id` and `access_token` | The tenant owns its own WABA |
Requesting inheritance when the organization has no WABA configured returns `422 VALIDATION_001`. Do not invent a hybrid, and do not describe the create payload as an Embedded Signup endpoint.
## Completion flow and callback
```json
{
"webHookUrl": "https://provisioning.example.com/callbacks/profile-complete",
"sandbox": false
}
```
Responses: `202` means processing started and contains no final status; `200` means the profile was already complete and the body carries a status.
The callback body is `{profileId, success, status, timestamp}`, documented with `COMPLETED`, `SUBMITTED`, and `failed`. It is delivered **once, with no retry**, which drives three requirements: the receiver must be reachable before the completion call, the receiver must be idempotent on `profileId`, and a reconciliation job must poll `GET /v3/profiles/{profileId}` for profiles that have been awaiting completion beyond a timeout.
Note that this callback is not part of the `/v3/webhooks` subscription system and is not documented as carrying the `x-webhook-signature` scheme. Give each provisioning record a unique, hard-to-guess callback path, treat its payload as untrusted input, verify `profileId` against the record you created, and never take action on an unrecognized identifier. Polling the profile remains the recovery and reconciliation authority.
## Status vocabulary by surface
| Surface | Observed values |
| --- | --- |
| Create response | lowercase `incomplete` |
| Completion `200` | lowercase `completed` |
| Completion callback | `COMPLETED`, `SUBMITTED`, `failed` |
| `GET /v3/profiles/{profileId}` guide | `approved`, `submitted`, `processing`, `failed` |
| REST guides versus OpenAPI | Publish different status sets |
Handle this by comparing case-insensitively, preserving unknown strings verbatim, recording which surface produced the value, and never switch-casing over an assumed closed enum. A provisioning state machine should treat any unrecognized status as "needs human review" rather than as an error.
## Error catalog for provisioning
| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `VALIDATION_001` | Invalid payload, including a `brand` object alongside `inherit_tcr_brand: true` |
| 400 | `VALIDATION_001` | Cannot create campaigns when `inherit_tcr_campaign` is true, or the campaigns are read-only |
| 403 | `AUTH_004` | Profile key attempted `x-profile-id`, or insufficient role |
| 404 | `RESOURCE_005` | Organization not found |
| 404 | `RESOURCE_014` | Profile not found |
| 404 | `RESOURCE_009` | Brand not found for the profile |
| 404 | `RESOURCE_010` | Campaign not found |
| 422 | `VALIDATION_001` | Organization has no WABA configured |
The response envelope carries `error.code`, `error.message`, `error.details`, and `error.doc_url`, plus `meta.request_id`. Log `request_id` for every provisioning call; it is the correlation handle for support.
## Idempotency and sandbox
`Idempotency-Key` is honored on POST, PUT, and PATCH with a value of 1 to 255 characters from `[A-Za-z0-9_-]`. Successful responses are cached 24 hours per key per customer, replays return the cached body with `Idempotent-Replayed: true` and `X-Original-Request-Id`, a duplicate arriving while the original is in flight waits up to five seconds and then fails `409 CONFLICT_001`, and if the idempotency store is unavailable the API returns `503 SERVICE_001` rather than risk a double execution. Use a deterministic key derived from your own provisioning record so a retry after a network timeout cannot create a second profile.
`"sandbox": true` authenticates and validates without persisting, queueing, calling providers, deducting balance, or looking up resources. Use it to prove a payload shape in CI. A successful sandbox mutation is itself cached by idempotency, so use a distinct key for the later live mutation or the live call will replay the sandbox response. Sandbox does not protect deletions in the webhook API, so never rely on it as a general dry-run guard.
## Offboarding
Deprovisioning a tenant is an ordered, evidence-preserving sequence rather than a single delete:
1. Stop new sends at the application layer.
2. Disable or delete the tenant's API keys in the dashboard.
3. Remove or downgrade the tenant's users, keeping at least one admin on the organization.
4. Disable webhook registrations that pointed at tenant infrastructure.
5. Detach shared resources deliberately, checking whether other profiles inherit from them.
6. Retain delivery and consent records for the applicable retention period before deleting the profile.
7. Record the `request_id` and timestamp of each step as the audit trail.
Contact deletion dissociates the contact from the calling customer while shared contact, capability, and delivery records persist, so deletion is not an erasure mechanism.
references/users-and-roles.md
# Users, roles, and credential hygiene
## Table of contents
- [User operations](#user-operations)
- [Role model](#role-model)
- [How a role check is evaluated](#how-a-role-check-is-evaluated)
- [Invitation lifecycle](#invitation-lifecycle)
- [Guardrails on user mutations](#guardrails-on-user-mutations)
- [API key model](#api-key-model)
- [Key rotation](#key-rotation)
- [Least-privilege assignments](#least-privilege-assignments)
- [Audit evidence](#audit-evidence)
## User operations
| Operation | Required role | Body | Notes |
| --- | --- | --- | --- |
| `GET /v3/users` | `developer` or higher | — | Lists active, invited, and suspended users |
| `POST /v3/users` | `admin` | `email`, `name`, `role`, optional `sandbox` | `201` with status `invited`; `409 RESOURCE_007` if the user exists |
| `GET /v3/users/{userId}` | `developer` or higher | — | Single user detail |
| `PATCH /v3/users/{userId}` | `admin` | `role`, optional `sandbox` | Role change only |
| `DELETE /v3/users/{userId}` | `admin` | optional `sandbox` | Removes access |
All five accept `x-profile-id` on an organization key to act on a child profile. None is exposed through MCP.
The user object returns `id`, `email`, `name`, `role`, `status`, `invited_at`, `last_login_at`, `created_at`, and `updated_at`.
The roles reference documentation states that the two read operations require "any role," while the OpenAPI specification requires `developer` or higher. Follow the specification and design for `developer` as the read floor; a `billing`-only user should not be assumed able to list users.
## Role model
| Role | Assignable by API | Scope of access |
| --- | --- | --- |
| `owner` | No | The account that created the organization or profile. Implicit, absent from the user list, passes every role check |
| `admin` | Yes | Passes every role check: profile management, user management, and reads |
| `developer` | Yes | Passes any-role checks; dashboard access to development, messaging, number lookup, activities, API keys, webhooks, channels, and settings |
| `billing` | Yes | Passes any-role checks; dashboard access limited to billing |
Note the practical implication of `developer` including API keys and webhooks: a developer can create credentials and change event destinations. Treat the role as privileged even though it cannot manage users.
## How a role check is evaluated
The check resolves against the email address that owns the API key. It passes when that email is the owner email, or when an **active** user with that email exists on the organization or profile holding an allowed role. Users in `invited`, `suspended`, or `rejected` status fail every check.
For a Sender Profile, checks cascade upward: owner or role access at the organization level also grants access to the organization's profiles. A user can therefore hold different effective roles across profiles when invited separately, while an organization-level role applies everywhere beneath it.
Two operational consequences. First, an invitation that has not been accepted grants nothing, so provisioning automation must not assume access after `POST /v3/users` returns `201`. Second, revoking access at the organization level is the only way to remove cascading access; deleting a profile-level user leaves an organization-level grant intact.
## Invitation lifecycle
```text
POST /v3/users → status "invited" → email with token → user accepts → status "active"
└── 7 days elapse → token expires → re-invite required
```
Inviting an email that already has access returns `409 RESOURCE_007`; read the user list first and decide between a role change and an invitation. A provisioning flow that invites tenant staff should record the invitation timestamp and re-invite after expiry rather than retrying blindly, and should verify `status == "active"` before assuming the user can act.
## Guardrails on user mutations
The API refuses to let a caller change their own role, demote the last admin, remove themselves, or remove the last admin. Rather than discovering these as validation errors, check first:
1. `GET /v3/users` and identify the target plus the count of active admins.
2. Confirm the target is not the caller's own account.
3. Confirm the change leaves at least one active admin.
4. Present the intended change and require explicit confirmation from the operator immediately before the call.
5. Record the `meta.request_id` from the response.
Role changes and removals are effectively irreversible from the target user's perspective — re-granting requires a fresh invitation and acceptance — so treat both as destructive operations that deserve a stated diff before execution.
## API key model
There are two key types. An organization key can act for a child profile by sending `x-profile-id` with the profile UUID. A profile-scoped key is confined to its own profile and receives `403 AUTH_004` if it sends `x-profile-id`.
Rate-limit exposure follows the key type: a profile key draws on its own pool, while an organization key acting through `x-profile-id` draws on the organization pool, so one noisy integration can consume quota shared by every profile.
There is **no** endpoint to list, create, or revoke API keys. Key management happens in the Sent Dashboard, where the value is masked in the table and copied with a control. Any runbook that claims to automate key creation is wrong.
Failed authentication is tracked per presented credential rather than per IP address: ten consecutive failures lock that credential with a `429` and escalating lockout windows from one minute up to sixty. A retry loop against a bad key therefore extends its own outage, so authentication failures must stop retrying immediately and alert instead.
`x-sender-id` is legacy v1 and v2 terminology. It has no role in v3 authentication or routing.
## Key rotation
1. Create a replacement key in the dashboard.
2. Update the secret store and redeploy so the new key is in use.
3. Verify with `GET /v3/me` that the new key resolves to the expected account.
4. Disable or delete the old key.
When a key is known to be compromised, invert the first steps and delete the old key immediately, accepting the brief outage. Keep separate keys per environment so rotating production never touches development, and never place a key in a browser, mobile app, or any client the organization does not control.
## Least-privilege assignments
| Workload | Credential | Role |
| --- | --- | --- |
| Server-side sends for one tenant | Profile-scoped key | `developer` |
| Provisioning new profiles | Organization key | `admin` |
| Campaign and brand registration | Organization or profile key per ownership | `admin` |
| Analytics reads | Profile-scoped key where possible | `developer` |
| Webhook management | Key matching the webhook's scope | `developer` |
| Billing review | — | `billing` |
| User administration | Organization key | `admin` |
Prefer profile-scoped keys for runtime send paths so a leak is contained to one tenant, and reserve organization keys for control-plane operations that genuinely require cross-profile reach.
## Audit evidence
Every response carries `meta.request_id` and `meta.timestamp`. Log both alongside the operation name, the acting credential identifier, and — when an organization key acted through `x-profile-id` — the target profile, since the credential alone does not reveal which tenant was affected. The Sent Dashboard's Activities section, visible to owner, admin, and developer roles, is the platform-side counterpart. Because there is no API for key inventory, maintain a written register of which key exists for which environment and workload, who owns it, and when it was last rotated.
SKILL.md
---
name: sent-profile-provisioning
description: Executes the Sent Sender Profile lifecycle over the API — creating profiles with the right inheritance, sharing, billing, and WhatsApp options, driving profile completion and its callback, managing 10DLC campaigns per profile, and administering users and roles. Use when calling POST /v3/profiles, handling a completion callback or unclear profile status, choosing inherit or dedicated resources, wiring per-tenant onboarding, or inviting and role-managing users.
---
# Sent Profile Provisioning
This skill is the execution counterpart to profile architecture: once the tenancy boundary is decided, it drives the API calls, the completion callback, the campaign registration, and the user administration that make a profile able to send. Design the boundary with `sender-profile-architect` first; provision it here.
## Provisioning sequence
1. **Confirm the credential.** `POST /v3/profiles` requires an organization key with `admin`. Profile-scoped keys cannot create profiles, and a profile key that sends `x-profile-id` receives `403`.
2. **Decide inheritance and sharing before the call.** These flags shape compliance posture and are awkward to unwind later.
3. **Create the profile**, validating the payload with `"sandbox": true` first when the shape is uncertain. Use a different idempotency key for the live create because a successful sandbox response is cached for 24 hours.
4. **Attach or inherit WhatsApp** via exactly one of the three supported paths.
5. **Register campaigns** for US SMS under the profile.
6. **Complete the profile** with `POST /v3/profiles/{profileId}/complete` and a reachable `webHookUrl`.
7. **Reconcile status** from the callback, or by polling if the callback is missed.
8. **Invite users** with least-privilege roles.
## Create payload essentials
`name` is the only required field. The consequential optional fields group into identity, sharing, inheritance, billing, WhatsApp, and brand.
```json
{
"name": "Northwind Retail",
"short_name": "Northwind",
"description": "Retail brand tenant",
"allow_contact_sharing": false,
"allow_template_sharing": false,
"inherit_contacts": false,
"inherit_templates": false,
"inherit_tcr_brand": true,
"inherit_tcr_campaign": true,
"billing_model": "profile",
"billing_contact": {
"name": "Ada Ops",
"email": "ops@example.com",
"phone": "+14155550100",
"address": "1 Example Way, Springfield"
},
"sandbox": true
}
```
`short_name` must be 3 to 11 characters of letters, numbers, and spaces with at least one letter. Inheritance flags default to true, so a profile created with no flags consumes the organization's contacts, templates, brand, and campaigns. The example opts into contact and template isolation explicitly while inheriting the organization's compliance registrations. Sharing flags expose this profile's resources outward; inheritance flags consume the organization's resources inward. They are independent directions and are frequently confused.
Create permits `name` alone, but completion also requires `short_name`, `description`, profile KYC information, and any required campaign or channel setup. When `inherit_tcr_brand` is true, the API rejects a `brand` object in the create request even though the profile still needs its own KYC submission; complete that KYC through the dashboard before calling the completion endpoint.
`billing_model` accepts `profile`, `organization`, or `profile_and_organization`. Any model that includes `profile` requires `billing_contact` when none exists, and `payment_details` is only accepted for those models. Card fields are forwarded to the payment processor and must never be logged, echoed, or persisted anywhere in the application.
Field-by-field rules, error codes, and the update-only fields are in [references/profile-lifecycle.md](references/profile-lifecycle.md).
## Inheritance decisions
| Flag | `true` means | Consequence |
| --- | --- | --- |
| `inherit_tcr_brand` | Use the organization's registered brand | A `brand` object in the same request is rejected |
| `inherit_tcr_campaign` | Use the organization's campaigns | Those campaigns are read-only for this profile; creating one returns a validation error |
| `inherit_contacts` | Read the organization's contacts | No contact isolation between tenants |
| `inherit_templates` | Read the organization's templates | No template isolation between tenants |
An inherited brand with `inherit_tcr_campaign: false` is a supported and common pattern: shared legal identity, dedicated messaging use cases per tenant.
## WhatsApp: exactly three paths
1. Organization Embedded Signup, performed in the Sent Dashboard. **No public endpoint starts this flow.**
2. Child-profile inheritance — omit `whatsapp_business_account` once the organization has a WABA.
3. Dedicated profile credentials — supply `whatsapp_business_account` with `waba_id` and `access_token`, optionally `phone_number_id`.
Supplying credentials on `POST /v3/profiles` is not an Embedded Signup endpoint. Omitting `whatsapp_business_account` when the organization has no WABA configured returns `422`; complete organization Embedded Signup or supply valid direct credentials. Use `waba-embedded-signup` for the operational signup flow.
## Completion and status
`POST /v3/profiles/{profileId}/complete` requires `webHookUrl`.
```json
{
"webHookUrl": "https://provisioning.example.com/callbacks/profile-complete",
"sandbox": false
}
```
A `202` means processing started and carries no final status. A `200` means the profile was already complete and its body carries a status. The callback body is `{profileId, success, status, timestamp}` and is **delivered once with no retry**, so the receiver must be live before the call and the flow must degrade to polling `GET /v3/profiles/{profileId}`. This callback is separate from subscribed Sent webhooks and is not documented as carrying the webhook HMAC headers; use a unique callback path tied to the provisioning record, reject unknown profile ids, and treat polling as the authoritative recovery path.
Profile status vocabulary differs by surface: the create response demonstrates lowercase `incomplete`, the completion `200` demonstrates lowercase `completed`, the completion callback uses `COMPLETED`, `SUBMITTED`, and `failed`, and `GET /v3/profiles/{id}` documents `approved`, `submitted`, `processing`, and `failed`. Do not assert a closed enum, do not lowercase-normalize into a fixed set, and record which surface produced each value. Compare statuses case-insensitively and preserve unknown strings.
## Campaigns per profile
Campaign management lives under the profile: `GET|POST /v3/profiles/{profileId}/campaigns` and `PUT|DELETE /v3/profiles/{profileId}/campaigns/{campaignId}`. There are no standalone brand endpoints; a dedicated brand is created with the profile.
<!-- sent-campaign-request -->
```json
{
"campaign": {
"name": "Northwind order notifications",
"description": "Order and delivery notifications for opted-in Northwind customers.",
"type": "App",
"useCases": [
{
"messagingUseCaseUs": "ACCOUNT_NOTIFICATION",
"sampleMessages": [
"Northwind: Your order 12345 has shipped. Reply STOP to opt out."
]
}
],
"volume": "1500",
"messageFlow": "Customers opt in at checkout before notifications begin.",
"privacyPolicyLink": "https://example.com/privacy",
"termsAndConditionsLink": "https://example.com/terms"
}
}
```
`messagingUseCaseUs` accepts one of thirteen values, `sampleMessages` holds 1 to 5 entries of at most 1,024 characters each, and a numeric `volume` string below 2,000 selects the low-volume tier while 2,000 or above selects the standard tier. Campaign statuses are `SENT_CREATED`, `ACTIVE`, and `EXPIRED`. Use `sms-10dlc-registration` for use-case selection and sample-copy policy.
## Users and roles
Five operations administer access: `GET /v3/users`, `POST /v3/users` (invite), `GET /v3/users/{userId}`, `PATCH /v3/users/{userId}` (role), and `DELETE /v3/users/{userId}`. None is exposed through MCP. Assignable roles are `admin`, `billing`, and `developer`; `owner` is implicit for the creating account and never appears in the list. Mutations require `admin`.
Role checks resolve against the email that owns the API key and pass only for the owner or an **active** user with an allowed role — `invited`, `suspended`, and `rejected` users fail. Organization-level access cascades to child profiles. Invitations expire after seven days, and inviting an existing user returns `409`.
Before any user mutation, read the current state, then confirm explicitly with the operator. The API refuses to let you change your own role, demote the last admin, remove yourself, or remove the last admin, but checking first produces a clear explanation instead of a validation error. The full role matrix and key-hygiene rules are in [references/users-and-roles.md](references/users-and-roles.md).
There is no endpoint to list, create, or revoke API keys; key management is a dashboard operation. Rotation is create-new, deploy, verify with `GET /v3/me`, then disable or delete the old key — deleting first only when the key is compromised.
## Multi-tenant provisioning notes
Webhook events never carry your application's tenant identifier. Before the first send, persist `message_id -> {tenant, profile, logical_send_id, channel}` and `receiving_number -> {tenant, profile}`. Do not infer tenant ownership from `account_id`, since many tenant profiles can share one organization. Provision one webhook registration per environment so a failing lower-environment receiver cannot auto-disable production.
## Boundaries
Use `sender-profile-architect` for the isolation, credential, and blast-radius design decision; `waba-embedded-signup` for the WhatsApp signup flow; `sms-10dlc-registration` for brand vetting and campaign policy; and `sent-webhook-engineer` for subscribed message-event receivers. Profile-completion callbacks use the separate verification and polling guidance in this skill.