Documentation menu

Providers

Sent

Uses the Ahena CLI (beta). Install it with npm install -g @ahena/cli, or prefix commands with npx @ahena/cli. See Installation. The dashboard covers projects, connections, Doctor, the Stack Graph, plans and approvals without it.

Overview

Sent (sent.dm) is one messaging API for SMS, WhatsApp and RCS, with automatic channel routing and fallback, templates that adapt per channel, sender profiles, and delivery webhooks. Ahena checks that your account, sender profile and channels are ready to send (including 10DLC and other registrations), creates the message templates and the delivery webhook your app declares, and generates the provider-neutral ahena.sms code. Your app sends straight to Sent with its own API key. Ahena never sends a message and never reads contacts, conversations or message history.

Package: @ahena/provider-sent · Category: sms Capabilities: sms, whatsapp, rcs, sender-profiles, registration, templates, webhooks

Maturity: verified against a fake of the API; not yet verified against the real service.

Connection

Field Secret Used for
SENT_API_KEY yes Every request, as the x-api-key header.
SENT_API_KEY=… ahena connect sent -e production

A key belongs to one account, which GET /v3/me reports as an organization (it has sender profiles), a standalone user, or a sender profile. An organization key can act for one of its profiles with the x-profile-id header: set sms.senderProfile (the profile's id or name) and Ahena sends that header on everything it reads or writes. A profile's own key can act only as that profile.

Sent's keys are UUIDs with one format for every environment; sandboxing is a per-request body flag ("sandbox": true), not a kind of key. So Ahena can't tell a "test" key from a "live" one and never reports live on validation. (The OpenAPI document's security-scheme description mentions sk_live_/sk_test_ prefixes; the authentication reference says otherwise, and Ahena follows the reference.) Use a separate key per environment so each can be revoked alone.

Permissions

Sent keys have no scopes. The key's account may hold a role (owner, admin, developer, billing), but Sent role-checks only profile and user management. Templates, webhooks, channels and /v3/me need nothing beyond a valid key, and no endpoint reports what a key may do. Ahena therefore declares permissionCheck: authentication-only: ahena connect proves the key works, which is everything Sent can say. A denial shows up as AUTH_004 (403) in Doctor or apply.

Recommendations:

  • Create a key just for Ahena (Development → API Keys) and give your app a different one.
  • Ahena never sends messages (it doesn't call POST /v3/messages), never reads contacts, conversations or messages, and drops webhook signing secrets, profile API keys, WhatsApp tokens, RCS agent contact details and brand/campaign KYC data from every response before anything is returned.

Capabilities

Capability Access Verification Changes Refuses
sms read-only Sending; buying numbers or adding SMS markets; filing 10DLC brands or campaigns
whatsapp read-only Connecting or changing a WhatsApp Business Account
rcs read-only Requesting an RCS agent
sender-profiles read-only Creating, editing or deleting sender profiles
registration manual
templates writable existence-only CONFIRMATION_REQUIRED Editing an existing template's content, category or language; deleting; AUTHENTICATION templates
webhooks writable read-back CONFIRMATION_REQUIRED Deleting or turning off webhooks; removing subscriptions; rotating secrets

Templates are existence-only: Sent's template response has a name, status, category, language, channels and variable names, but not the copy, so Ahena confirms the template exists (and Doctor reports its approval and any difference in variables, category or language); it can't compare the text. Webhooks are read back in full (URL, subscriptions, on/off); the signing secret is never readable after creation.

Network: api.sent.dm only, enforced. Your webhook route is probed through Ahena's credential-free probe. ahena lock tracks the acting profile, the SMS markets, WhatsApp and RCS status, the declared templates' status and the webhooks.

Configure

sms: {
  provider: "sent",
  senderProfile: "…",                // optional: a sender profile id or name (organization keys)
  channels: ["sms", "whatsapp"],     // optional, default ["sms"]: what Doctor checks is ready
  templates: [
    {
      name: "order_shipped",
      body: "Hi {{name}}, your Leo order {{order}} is on its way. Reply STOP to opt out.",
      samples: { name: "Lucas", order: "1042" },
      category: "UTILITY",            // optional: MARKETING or UTILITY (Sent detects it otherwise)
      language: "en_US",              // optional (Sent detects it otherwise)
      submitForReview: true,          // optional, default true
    },
  ],
  webhook: { production: { endpoint: "https://…/api/webhooks/sent", events: ["message", "templates"] } },
},

Any key may be keyed by environment. body becomes Sent's shared multiChannel body, so the same copy renders on SMS, WhatsApp and RCS; each {{name}} becomes Sent's {{index:variable}} placeholder with the sample you give (reviewers see it). webhook.events takes Sent's event types (message, templates, channel, contact, link, call) or one sub-type (message.delivered); Ahena checks them against GET /v3/webhooks/event-types. Default: message and templates.

Change Classification Notes
Create a template (POST /v3/templates, then PUT to set its name) CONFIRMATION_REQUIRED Matched by name, so never duplicated. Sent has no name on create, so Ahena names it right after. Submitted for review unless submitForReview: false. The request carries an Idempotency-Key, so a retry after a lost response returns the same template.
Submit a DRAFT template for review (PUT … {"submit_for_review": true}) CONFIRMATION_REQUIRED Only submit_for_review; the content isn't touched.
Create the delivery webhook (POST /v3/webhooks) CONFIRMATION_REQUIRED Matched by URL. Named Ahena: <environment>. Sent returns the signing secret once; Ahena stores it encrypted as SENT_WEBHOOK_SECRET and never returns it.
Add missing subscriptions to that webhook (PUT /v3/webhooks/{id}) CONFIRMATION_REQUIRED The webhook's name, URL, retry count and timeout are sent back as read; subscriptions are only widened, never removed.
Turn the webhook back on (PATCH …/toggle-status) CONFIRMATION_REQUIRED Sent turns a webhook off after 10 consecutive failed events. Fix the route first.

Template approval is asynchronous (Meta reviews it when a WhatsApp Business Account is connected, Sent's compliance team otherwise, per channel). Creating a template is VERIFIED once it exists; while it waits, Doctor reports sent.template.<name> as WARNING with a MANUAL fix ("wait for the review"), and Sent's templates.approved / templates.rejected webhook events say when it's decided.

Refused outright: deleting anything; sending; editing an existing template's content (editing an approved template takes it out of service until it's approved again; declare new copy under a new name instead); template names starting with sent_ (reserved by Sent); webhook URLs that aren't public https or embed credentials; numbers, SMS markets, WhatsApp, RCS, sender profiles and 10DLC registrations (they're billed, registered or approved outside the API's reach, so they're MANUAL).

Generate

ahena generate sent [--framework nextjs]:

  • src/ahena/sms/index.ts: the provider-neutral interface shared with Twilio: export interface SendSms { to: string; body: string } and export const sms = { async send(input): Promise<{ id: string }>, get raw() }. send maps body to Sent's free-form text on the sms channel and returns the recipient's message_id. Sent delivers free-form text only inside an open conversation: to a contact who never replied, send an approved template first, through sms.raw (the @sentdm/sentdm client): sms.raw.messages.send({ to: ["+1…"], template: { name: "order_shipped", parameters: { … } } }). Otherwise Sent accepts the send (202) and the message ends BLOCKED with CONVERSATION_TEMPLATE_REQUIRED, reported on the message.blocked webhook event.
  • src/ahena/providers/sent.ts: the client, from SENT_DM_API_KEY (the SDK's own variable), and sentProfile() for the x-profile-id header from SENT_PROFILE_ID.
  • .env.example.sent.
  • src/app/api/webhooks/sent/route.ts (Next.js): verifies Sent's Svix-style signature before anything else: v1, + base64 HMAC-SHA256 over {X-Webhook-ID}.{X-Webhook-Timestamp}.{raw body} with the base64-decoded secret (without whsec_), compared in constant time, within 5 minutes. 401 when it doesn't verify, 500 when SENT_WEBHOOK_SECRET isn't set, 400 for invalid JSON, 200 after handling. The SDKs don't verify webhooks, so this is the code to keep.

Limitations

  • Ahena doesn't create sender profiles, buy numbers, add SMS markets, connect WhatsApp, request RCS agents, or file 10DLC brands and campaigns. They're billed or reviewed by carriers, Meta or The Campaign Registry. Doctor reports their status and the steps.
  • A test key in production can't be detected: Sent has one key format.
  • Ahena never edits or deletes an existing template, and can't read its copy back (only its variables, category, language and status), so a change to body is reported as drift, not applied.
  • AUTHENTICATION templates (Meta's OTP format), headers, footers, buttons, channel-specific bodies and MMS media aren't modelled: create those in the Sent Dashboard.
  • Organization webhooks cloned onto every sender profile (sender_profile) aren't managed. Ahena creates the webhook on the account it acts as.
  • inspect shows the key's own account (and its profiles' names and status); channel status for a profile shows in Doctor and ahena lock when senderProfile is set.
  • The template and webhook Idempotency-Keys are cached by Sent for 24 hours. Ahena double-checks a replayed create and creates for real if the earlier one was deleted since.
  • The @sentdm/sentdm SDK (0.42) has no /v3/sender-profiles or /v3/channels resources and its Template type claims a definition the API doesn't return; Ahena follows the API reference and OpenAPI document. Not run against the real Sent API yet.

Manual steps

  1. Create a sender, connect WhatsApp or request RCS in the Sent Dashboard (Channels) as Doctor lists; for US long codes, complete the 10DLC brand and campaign.
  2. Finish a sender profile's setup (Sender Profiles) if Doctor says it's incomplete.
  3. Wait for template reviews; revise and resubmit a rejected template in Sent, or declare new copy under a new name.
  4. After deploying the webhook route with SENT_WEBHOOK_SECRET, send a test event from the webhook's page in the Sent Dashboard.

Doctor checks

Id Severity Meaning
sent.credentials PASS / FAIL The key works; the account type and name.
sent.sender_profile PASS / WARNING / FAIL The declared profile exists and this key can act for it; its setup status (approved, or WARNING while incomplete/pending, FAIL when rejected).
sent.channel.sms PASS / INFO / WARNING / FAIL At least one SMS market is ACTIVE; no sender at all (FAIL in production, MANUAL).
sent.channel.sms.market.<country>_<type> WARNING A market isn't ACTIVE yet (10DLC, sender ID or document review), with Sent's reason and whose move it is.
sent.channel.whatsapp / sent.channel.rcs PASS / INFO / WARNING / FAIL Declared channel connected and ACTIVE; ACTION_NEEDED or INACTIVE fail; under review warns in production.
sent.channel.unknown WARNING sms.channels names something other than sms, whatsapp, rcs.
sent.template.<name> PASS / WARNING / FAIL Approved; waiting for review (MANUAL); DRAFT or missing (plannable fix); rejected, paused, disabled or revoked (MANUAL); several with the same name.
sent.template.<name>.drift WARNING Variables, category or language differ from ahena.config.ts.
sent.webhook PASS / WARNING / FAIL A webhook delivers to the endpoint, is on, and has the declared subscriptions (plannable fixes).
sent.webhook.events FAIL webhook.events names an event type Sent doesn't have.
sent.webhook.failures WARNING Consecutive failed deliveries (Sent turns the webhook off at 10).
sent.webhook.endpoint PASS / WARNING / FAIL One unsigned POST to your route: 401/403 is right; 2xx means it doesn't verify signatures; 404/405 offers a SAFE generate fix.
sent.channels / sent.templates FAIL Sent couldn't be read (the error says why, e.g. AUTH_004).

Core also checks that SENT_WEBHOOK_SECRET is stored (by name) when a webhook is declared.

Disconnect behavior

Removes the connection and the key Ahena stored. Sent has no API to revoke keys: delete or disable Ahena's key in the Sent Dashboard → Development → API Keys. If your app uses the same key it stops sending, which is why Ahena should have its own. Sender profiles, channels, templates, webhooks and message history are untouched.

Set up step by step

  1. Create the credential. Sent Dashboard (app.sent.dm) → Development → API Keys → Add API Key, name it ahena-production, Create Key, and copy it. Use the organization's key if Ahena should manage a sender profile (then set sms.senderProfile), or the profile's own key.

  2. Minimum permissions. Sent keys have no scopes; any active key works. Its owner needs no admin role for what Ahena does (templates and webhooks aren't role-gated).

  3. Connect.

    SENT_API_KEY=… ahena connect sent -e production

    Pipe the key or type it at the hidden prompt; never pass it as an argument.

  4. Verify the connection. ahena inspect sent -e production lists the account, its sender profiles, SMS markets, WhatsApp and RCS status, templates and webhooks (no secrets).

  5. Run Doctor. ahena doctor -e production.

  6. Plan. Add the sms section above to ahena.config.ts, then ahena plan -e production (or ahena configure sent -e production to plan and apply one provider).

  7. Apply an allowed change. ahena apply -e production. Creating the template and the webhook each need confirmation; the signing secret is stored as SENT_WEBHOOK_SECRET.

  8. Verify. ahena plan -e production shows no Sent changes; ahena doctor -e production shows the webhook PASS and the template waiting for review (WARNING) until it's approved; ahena env secrets production lists SENT_WEBHOOK_SECRET (masked). Then ahena generate sent -e production --framework nextjs, set the variables from .env.example.sent (ahena env reveal production SENT_WEBHOOK_SECRET), deploy, and check sent.webhook.endpoint passes.

  9. Troubleshooting.

    Symptom Cause Fix
    "Sent rejected the API key" Key mistyped, disabled or deleted Create a key (Development → API Keys) and reconnect.
    "locked this API key … (BUSINESS_002)" 10 failed authentications in a row Wait (1 minute, escalating), fix the key, retry.
    sent.sender_profile FAIL "Only an organization key…" A profile key with another profile's senderProfile Connect the organization's key, or that profile's own.
    sent.channel.sms.market.us_ten_dlc WARNING 10DLC brand or campaign still in review, or something owed Follow the reason Doctor shows; reviews take days.
    sent.template.<name> WARNING "waiting for review" Meta or Sent hasn't decided yet Nothing to do; it stays BLOCKED for sends until approved.
    Apply: "CONFLICT_006" The template is in review; content is frozen Wait for the review to finish.
    Messages BLOCKED with CONVERSATION_TEMPLATE_REQUIRED sms.send free-form text to a contact with no open conversation Send an approved template first via sms.raw.
    sent.webhook.endpoint FAIL "accepted an unsigned request" The route doesn't verify signatures Use the generated route (or verify X-Webhook-Signature first).
    sent.webhook "turned off" 10 consecutive failed events Fix the route, then ahena configure sent -e production.
  10. Disconnect and revoke. ahena disconnect sent -e production, then delete or disable the key in the Sent Dashboard → Development → API Keys.