Messaging

Phone numbers

Order a Brazilian WhatsApp-ready number through Salvy (an owner or admin approves each production rental), or connect one you already own. One row per number, one provider forever.

For branding, business verification, marketing permission and review next steps, see Keep your WhatsApp account in good standing.

Two sources

SourcePathDDD required
salvyTyxter provisions the number via SalvyYes
byonYou bring your own Meta-connected numberNo

Source also determines control and exit behavior. See WABA and phone-number ownership before choosing a long-lived sender: BYON stays under your control, while a Salvy number is a Tyxter-managed rental with no published port-out guarantee.

List available regions

GET/v1/phone-numbers/available-regions

Returns DDDs Salvy currently has stock for, with the monthly BRL rental fee for each.

Rent a Salvy number: orders and approval

In production, complete the legal business-identity declaration first. When the default-off deployment policy PHONE_PROVISIONING_REQUIRE_BILLING_WHATSAPP is enabled, complete the separate billing WhatsApp alert step in dashboard Settings → Business identity too: a current owner or admin must have a dashboard-verified WhatsApp identity and explicitly consent to transactional billing alerts. This is not the legal KYC contact phone. Under that policy, order creation and provision return 403 billing_whatsapp_required before an order is written, and the approval checks it again; existing rentals, sandbox, and BYON connect are unaffected.

In production, Tyxter rents a number only after an owner or admin of the organization approves it. Each production number is a phone line held in your organization's name, and a person on your team must be responsible for it. So an owner or admin authorizes every rental themselves; an agent or API key cannot. A standard API key with phone_numbers:write can ask for a number; agent keys cannot hold that scope.

POST/v1/phone-numbers/orders
curl https://api.tyxter.com/v1/phone-numbers/orders \
  -H "authorization: Bearer $TYXTER_API_KEY" \
  -H "content-type: application/json" \
  -H "idempotency-key: order-sales-line-1" \
  -d '{ "ddd": "11", "display_name": "ACME Suporte", "purpose": "Support line" }'

A production order answers 201 with status: "awaiting_approval", a priced preview of the activation fee and each later charge, terms_version, expires_at (24 hours after the preview) and approval_url. Nothing is rented, reserved or charged yet. Hand approval_url to an owner or admin: it opens the order's approval page in the dashboard, which shows the order's charge summary, the rental terms and a link to the renewal policy. Any organization member with access to the order's project can view the order there (owners and admins always can), and only an owner or admin can approve or decline it. No API route approves an order, and no webhook or notification announces one, so relaying the link is how the approver learns about it.

In the dashboard, an owner or admin who rents a number approves it in the wizard's confirm step. A member who starts one gets the approval link to pass to an owner or admin.

GET/v1/phone-numbers/orders/{order_id}

Read the order about once a minute until it reaches a final status. Reading spends no provision rate-limit token, but it is refused with 402 credit_balance_exhausted while production credit is exhausted.

StatusMeaning
awaiting_approvalWaiting for an owner or admin. After expires_at it reads expired.
approvedThe first month is reserved and provisioning is queued; phone_number_id is set.
provisioningThe number is being provisioned.
fulfilled (final)The number became active. It still needs Meta registration, below.
failed (final)The number never became active. When provisioning failed after the approval, the reservation is returned and the phone's error_code says why. A number released or disconnected before it became active also ends here, and a month already charged when the carrier delivered it is not refunded.
declined (final)An owner or admin declined it (phone_number_order_declined).
expired (final)Not approved within 24 hours (phone_number_order_expired), or an approval attempt found that the price, a charge date or the rental terms had changed (phone_number_order_terms_changed). Until someone attempts the approval, a read still shows awaiting_approval.

A declined or expired order rented and charged nothing: create a new order. Approving re-runs the business-identity, billing WhatsApp, plan allowance and Tyxter-provisioned number limit checks and needs the whole activation fee in the balance; a refused approval, such as 402 insufficient_balance, leaves the order awaiting approval until it expires. When the price or a charge date cannot be determined, creation answers 503 phone_number_rental_preview_unavailable and creates nothing; retry later.

Order creation and provision share one per-organization rate limit (burst 5, sustained 10/hour). A replay of the same Idempotency-Key returns the order as it was created, not its current status, and still spends a token first, so with an empty bucket it answers 429 rate_limited. Without a key every request creates a new order that needs its own approval. The provisioning feature control blocks order creation, never the order read or the dashboard decision.

Provision (Salvy)

POST/v1/phone-numbers/provision
curl https://api.tyxter.com/v1/phone-numbers/provision \
  -H "authorization: Bearer $TYXTER_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "ddd": "11", "display_name": "ACME Suporte" }'

In sandbox, provision returns 202 with a simulated number, and a sandbox order is approved on creation with a free preview: sandbox needs no approver, business identity or balance. In production, provision creates a rental order and returns 409 phone_number_rental_approval_required; error.details.order carries the order's id, approval_url and expires_at (TyxterApiError.rentalOrder in the JavaScript SDK). Hand the link to an owner or admin and read the order; do not also create one with POST /v1/phone-numbers/orders. A keyed replay answers the 409 stored at creation, so read the order for its status.

Migration note (October 2026): a production provision used to rent the number at once and return 202. It now returns the 409 above, with no deprecation window. Reading the order with GET /v1/phone-numbers/orders/{order_id} needs the phone_numbers:read scope, which a key that only had phone_numbers:write for provision lacks. A replay of an Idempotency-Key first used before the change still returns its original 202. Numbers rented before the change, their renewals, and sandbox provisioning are unchanged.

Once approved, the actual Salvy call runs on the provider.phone worker pool. Watch the status transitions:

requested  →  provisioning  →  provisioned  →  verifying  →  active

Salvy forwards the SMS verification code via webhook and we stash it on the row. Fetch with GET /v1/phone-numbers/:id to read verification_code and verification_code_received_at.

Register a Salvy number with Meta

Carrier activation and Meta registration are separate. A production Salvy number can be active while meta_phone_number_id is null; it is still pending Meta registration. Continue in the dashboard's WhatsApp connection flow.

  1. In Meta, choose SMS, not a voice call, for the verification method.
  2. The verification SMS reaches the Tyxter-provisioned number through Salvy. After Salvy receives it, the code appears in the Tyxter dashboard.
  3. Copy the code from the dashboard and enter it in Meta's registration wizard.

This is the Meta registration step for a Salvy number; POST /v1/phone-numbers/connect is for a number you already registered with Meta.

To move an already-registered number to a different WABA or Business Portfolio, run WhatsApp Embedded Signup again from dashboard Connections → Meta and select the target Business Portfolio, WABA, and number. No disconnect or release is required for this dashboard rerun; your Salvy rental stays in place. Read the move guide for relinking, template resync, and effects on other numbers.

Connect (BYON)

POST/v1/phone-numbers/connect
curl https://api.tyxter.com/v1/phone-numbers/connect \
  -H "authorization: Bearer $TYXTER_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "phone": "+5511999999999",
    "meta_phone_number_id": "109...",
    "display_name": "ACME Vendas"
  }'

BYON numbers skip Salvy and jump straight to verifying. You complete WhatsApp verification on your side; Tyxter transitions the row to active when that finishes.

Import a selected Salvy account number

POST/v1/phone-numbers/import-salvy

After connecting a customer Salvy account, choose one cached account-owned number and send its connection and resource ids with continuation_terms_version: "salvy_byok_v1" and an Idempotency-Key. Tyxter creates a customer-owned BYON phone and keeps the returned nullable salvy_management enrollment record. Listing inventory never imports it. The import call itself does not provision, release, or rent a Tyxter phone, call Salvy, or write wallet, fee, rental-cycle, or usage records.

Importing needs an active Standard or Growth plan; without one the call returns 403 salvy_byok_subscription_required. While the plan stays active, and through the end of the UTC month in which it ends, Tyxter charges nothing to manage an imported number. After that, keeping that management costs a monthly management fee (the phone.management.month meter): one full fee for each UTC calendar month, charged from your balance on the 1st, as the renewal policy describes. On the free plan, a number whose month is still unpaid at 12:00 UTC on the last UTC day of that month is disabled. While neither a plan nor a charged month covers the number, for example after it is disabled, sending from it fails with phone_service_suspended. The number is not released. Each number’s salvy_management on GET /v1/phone-numbers reports billing’s live coverage, next charge and state, with suspension_reason while suspended; GET /v1/billing/phone-management adds payment status and the disable deadline. End management for free with convert-to-byon below.

Complete imported Salvy registration

POST/v1/phone-numbers/{id}/salvy/complete-registration

Send the imported phone's Meta phone id and an Idempotency-Key. The response is 202 while Tyxter independently confirms that id belongs to the same environment's Meta WABA. A connected Salvy account or received SMS alone never activates WhatsApp messaging.

End optional customer Salvy management

POST/v1/phone-numbers/{id}/convert-to-byon

A customer-owned BYON phone can send exactly {} with anIdempotency-Key to keep the same phone and Meta link while its nullablesalvy_management becomes null. This free exit does not release or deregister the customer resource and stays available after a customer connection is disconnected. It is an idempotent no-op for ordinary BYON; platform Salvy rentals and foreign ids return the usual non-enumerating 404.

Meta display names

Every phone-number response, including list, retrieve, and mutation responses, includesverified_name, pending_name_review, and name_review.display_name remains the customer-entered label for the row; it is not Meta's verified display name.

In production, verified_name is populated only by Tyxter's scheduled Meta health sweep and is null before a completed sweep observes it. Sandbox returns a deterministic simulated verified name from its health snapshot. Reading either response never makes a provider call.

pending_name_review is the current pending Graph health observation, not a completed decision. It is either null or { requested_name: string | null; status: string | null; observed_at: ISO timestamp }. Its raw status is open Meta vocabulary, and observed_at is the same successful-sweep freshness fact as meta_health_synced_at. A successful complete production health sweep is authoritative and writes or clears the block. A completed callback can clear it immediately only when its requested name matches and its effective time is not older than observed_at; all other callbacks wait for the next sweep. A failed or partial refresh leaves its prior values and freshness untouched. Sandbox deterministically returns pending_name_review: null without calling Meta, and that null means no pending Graph observation — not that a review was approved.

name_review is null until Tyxter has durably received a completed callback decision. Once present it contains requested_name, decision, reason, and reviewed_at; requested_name and reason may be null. Treat decision as an open Meta string rather than a fixed enum. It is separate from both pending_name_review and the name_status in the existing health webhook snapshot.

A name-review update appears in the dashboard and can create an email notification when your notification preferences allow it. It does not add a signed customer webhook; API clients should read the phone resource for the current result.

Release a number

POST/v1/phone-numbers/{id}/release

Returns 202. Salvy numbers move to release_requested while a worker cancels the line at Salvy; the row reaches released as soon as Salvy accepts the cancellation or reports the line already gone. If Salvy fails otherwise, the number stays release_requested while Tyxter retries. BYON numbers move directly to released. A Tyxter-rented number keeps billing until you release it: the release request (status release_requested) stops further monthly charges. Disconnecting it with DELETE /v1/phone-numbers/{id} does not stop billing.

Billing

A number Tyxter provisions for you pays a monthly rental (the phone.number.month meter). Ordering a number in sandbox is free and reserves nothing. A number you bring yourself costs nothing to keep. An imported Salvy number is the exception from the 1st after the UTC month in which your Standard or Growth plan ends: it then pays the monthly management fee described under Renewal policy. These rules summarize How number rental is billed, which also gives dated examples.

Renewal policy

A number Tyxter rents for you renews every UTC calendar month, and the monthly rental is charged on the 1st of each month, for the month that starts. The renewal notices arrive by email and in the dashboard; each states what applies to that number at the moment it is sent, with every figure read from your account at that moment. This policy applies from the November 2026 renewal.

Imported Salvy numbers

A number imported from your own Salvy account pays no rental. Once your Standard or Growth plan ends, its monthly management fee (the phone.management.month meter) follows the same calendar:

Webhooks

The pause/resume pair fires on the transition, not per message, so a ten-thousand-message broadcast produces exactly one paused. The resume is OBSERVED rather than scheduled: Tyxter reports it when a send to a new recipient next succeeds, so a paused number that stops sending stays paused and emits nothing. Never wait for a resume before sending again — sending is what produces it. Read the remaining allowance itself from remaining_messaging_allowance_estimate on the number. A linked Meta phone reports its quality-adjusted estimate against shared portfolio usage there. An unlinked Meta phone uses the conservative per-phone fallback; sandbox follows its deterministic simulated phone tier. Read the per-message reason from status_reason on held messages.

The three health events each carry the full health snapshot — quality_rating, meta_quality_rating, messaging_tier, messaging_limit_tier, name_status, and meta_health_synced_at — plus the previous value of whichever dimension moved, so you never need a follow-up retrieve. They fire only on a real change: the first time a number’s health is read is not a change, and re-reading the same value emits nothing. Production only — a sandbox number never reaches Meta and takes a constant simulated health snapshot instead.

What to do when Tyxter paces a number

This is not a total-message limit. Meta limits the unique WhatsApp recipients a business can deliver messages to outside an open customer-service window during a moving 24-hour period. Here, “new recipient” means a recipient who does not already hold a slot in that rolling period — not a phone number you have never contacted before.

When the allowance is full, Tyxter holds only sends that need another slot. Messages to a recipient with an active slot, and messages inside that recipient's customer-service window, keep flowing. Slots expire one by one, so there is no daily reset time. A number stays paused until the messages deferred behind the pause have all been sent, so a busy sender does not flap between paused and resumed as individual slots free up. The dashboard can show an earliest time that a slot may free up, but it is an estimate, not a promise that sending resumes at that time.

The effective pacing cap applies the configured safety margin, then a quality ratio for the sending phone. Default ratios are GREEN 100%, YELLOW 50%, and RED 25%; unknown quality uses the configured GREEN ratio. With margin 100%, a raw allowance of 250 yields caps of 250, 125, and 62. This cap is a threshold, not a count of messages sent, and it does not replace the raw Meta health tier.

Linked Meta senders share Business Portfolio recipient usage. Each sending phone compares that shared usage with its own quality-adjusted cap. A GREEN sibling may have room when a YELLOW phone is paced, but moving traffic does not create a new portfolio allowance. Health recovery is picked up by automatic retries; it does not promise an immediate resume, and the deferred-backlog guard still applies.

For a finite pacing cap, pause notifications and emails describe a recorded Yellow or Red multiplier below 100%, before rounding. Alerts without a finite cap or quality context keep neutral wording. Dashboard phone detail shows current quality pacing context for finite caps, which can differ from the earlier alert. A Green rating does not mean queued messages have completed.

Watch the paused state on the dashboard phone roster and detail view, the paced label in message list and detail views, and the notification bell. A held message has status_reason: "messaging_limit_pacing"; see the message status_reason reference. It stays queued, retries automatically, and must not be resent or re-created — doing so can duplicate a customer message.

Subscribe webhook receivers to phone_number.messaging_limit_paused and phone_number.messaging_limit_resumed to observe the live transition. Existing endpoints keep their saved subscriptions, so endpoints created before these events may need both names added to subscribed_events. New endpoints created in the dashboard suggest the full event catalog, including both pacing events, by default.

For remediation, review the number's quality, verification, and current messaging-limit information in WhatsApp Manager and Meta's guidance. Follow Meta's Messaging Limits guidance and Account Quality; do not rely on a published promotion threshold because Meta can change its current criteria.

There is intentionally no transparent failover between Salvy and another provider. A number is always served by the provider that provisioned it. Fallback means releasing the row and ordering a new one, which an owner or admin approves.

The renewal field on phone reads

GET /v1/phone-numbers and GET /v1/phone-numbers/{id} return a required renewal field. It is null for BYON or sandbox. For a production number Tyxter rents for you, it reports the current UTC month under the renewal policy:

  • next_renewal_at is the next monthly charge, 00:05 UTC on the 1st. From November 2026, the first month charged on its 1st, until this month’s charge for the number is recorded, it stays on this month’s 1st, even after 00:05 UTC has passed; a number bought this month, whose first charge paid the month, reads next month’s 1st. It is null before the number is first active and once a release is requested.
  • State at_risk, with reason insufficient_credit and recommended_action add_credit_or_enable_auto_topup, means this month’s rental charges left the organization’s balance below zero, this number was charged for the month, and the balance has not come back to zero or above. State upcoming means the month is paid, or the number was not charged for it (a number bought this month pays it with its first charge).
  • On the free plan, release_cutoff_at is when the number is released if the month is still unpaid: 12:00 UTC on the month’s last UTC day. It is null when nothing is due to be released.
  • automatic_release_enabled is false while a paid plan keeps your numbers. An unpaid month then still reads at_risk, with no release_cutoff_at.
  • amount_brl is this month’s rental charge for the number, or null when it had none.
  • State release_pending means a release was requested; released means it is done. cycle_id, renewal_warning_48h_at, renewal_warning_24h_at, grace_selected_at, grace_ends_at, release_requested_at and provider_evidence_observed_at are always null.

Use phone_numbers:read to list the non-released roster in the key’s project and environment. Follow next_cursor as starting_after until has_more is false, then retrieve a phone to refresh it. Retained released rows remain retrievable while they exist. These two reads remain available during credit exhaustion and do not repair phone lifecycle, create billing work, charge, notify, or call providers. Authentication, scope, suspension and tenant checks still apply.

To top up, use the Billing page for the same organization and project, or use billing:write with POST /v1/billing/packages/purchase and Idempotency-Key, or PUT /v1/billing/auto-topup. Read the package or auto-top-up contract before submitting. Then refresh GET /v1/billing/balance with billing:read: production_blocked false means production API access is back, and a balance of zero or above means the month is paid, which the renewal field then reads as upcoming. Phone read scope does not grant billing access or expose the organization wallet, ledger or other projects’ obligations.

Unpaid month on the free plan

{
  "id": "pn_abc",
  "object": "phone_number",
  "source": "salvy",
  "status": "active",
  "environment": "production",
  "display_name": "Tyxter Brazil",
  "ddd": "11",
  "phone": "+5511987654321",
  "provider_number_id": "salvy_456",
  "meta_phone_number_id": "456_phone",
  "waba_id": null,
  "quality_rating": "unknown",
  "messaging_tier": "unknown",
  "messaging_limit_tier": null,
  "meta_throughput_tier": null,
  "meta_quality_rating": null,
  "verified_name": null,
  "pending_name_review": null,
  "name_review": null,
  "meta_health_synced_at": null,
  "current_24h_unique_recipients": 0,
  "remaining_messaging_allowance_estimate": 225,
  "verification_code": null,
  "verification_code_received_at": null,
  "monthly_fee_brl": "49.90",
  "error_code": null,
  "error_message": null,
  "created_at": "2026-04-25T10:00:00Z",
  "updated_at": "2026-04-25T11:30:00Z",
  "activated_at": "2026-04-25T11:30:00Z",
  "released_at": null,
  "recent_messages": [],
  "renewal": {
    "state": "at_risk",
    "reason_codes": [
      "insufficient_credit"
    ],
    "cycle_id": null,
    "amount_brl": "49.9000",
    "currency": "brl",
    "next_renewal_at": "2026-12-01T00:05:00.000Z",
    "release_cutoff_at": "2026-11-30T12:00:00.000Z",
    "renewal_warning_48h_at": null,
    "renewal_warning_24h_at": null,
    "grace_selected_at": null,
    "grace_ends_at": null,
    "automatic_release_enabled": true,
    "release_requested_at": null,
    "recommended_action": "add_credit_or_enable_auto_topup",
    "evaluated_at": "2026-11-10T12:00:00.000Z",
    "provider_evidence_observed_at": null
  }
}

Unpaid month on a paid plan

{
  "state": "at_risk",
  "reason_codes": [
    "insufficient_credit"
  ],
  "cycle_id": null,
  "amount_brl": "49.9000",
  "currency": "brl",
  "next_renewal_at": "2026-12-01T00:05:00.000Z",
  "release_cutoff_at": null,
  "renewal_warning_48h_at": null,
  "renewal_warning_24h_at": null,
  "grace_selected_at": null,
  "grace_ends_at": null,
  "automatic_release_enabled": false,
  "release_requested_at": null,
  "recommended_action": "add_credit_or_enable_auto_topup",
  "evaluated_at": "2026-11-10T12:00:00.000Z",
  "provider_evidence_observed_at": null
}

Paid month

{
  "state": "upcoming",
  "reason_codes": [],
  "cycle_id": null,
  "amount_brl": "49.9000",
  "currency": "brl",
  "next_renewal_at": "2026-12-01T00:05:00.000Z",
  "release_cutoff_at": null,
  "renewal_warning_48h_at": null,
  "renewal_warning_24h_at": null,
  "grace_selected_at": null,
  "grace_ends_at": null,
  "automatic_release_enabled": true,
  "release_requested_at": null,
  "recommended_action": "none",
  "evaluated_at": "2026-11-10T12:00:00.000Z",
  "provider_evidence_observed_at": null
}