Meta's official API
bZapper has two rails. On the unofficial one, a number is a session paired by QR code. On the official one, messages go out through the WhatsApp Cloud API, from the number that belongs to your customer's business account at Meta.
You pick the rail when creating the project, with one button. After that, the promise
is that nothing changes: POST /messages/text is POST /messages/text, the SDK is the
same, the webhooks are the same, the inbox is the same.
This page is about what only exists when Meta is the carrier.
What changes, in one table
| Unofficial rail | Official rail | |
|---|---|---|
| How a number joins | QR code in the panel | One Meta window, once |
| Starting a cold conversation | Free (with block risk) | Approved template only |
| Buttons and lists | Fall back to a text menu | Native (within Meta's limits) |
| Poll | Yes | Does not exist in the Cloud API |
| Groups | Yes | Do not exist in the Cloud API |
| Campaigns | Text with variations, 7-day ramp-up | Approved template, no ramp-up |
| Scheduling | Yes | Yes — the 24h window is evaluated when it goes out |
| Who charges per message | Nobody | Meta, on your customer's card |
| Ban risk | Real | Practically nil |
What does not exist on a rail does not pretend to: in an official project, the QR-number
routes (creating and pairing an instance, groups, editing or deleting a message,
blocking, labels, calls) answer 409 official_rail_unsupported; in a QR project,
/official/* and /templates answer 409 official_project_required. The full matrix,
route by route, is in the x-rails field of the API reference.
The billing deserves one more sentence: Meta charges each message directly to your customer, on the card they register in their own business account. bZapper neither intermediates nor adds a margin to that: it only charges the subscription. Your bZapper subscription is one thing; Meta's per-message fee is another.
Connecting the account
Once, in the panel, under Numbers. Your customer signs in with their Facebook account, picks (or creates) the business account, enters the number, confirms the code and authorises. It is the only moment they leave bZapper.
After that, one step remains that Meta does not allow through the API: attaching a card to the business account. Without it, Meta refuses the sends. The panel explains it with the exact link, and when the card lands we detect it ourselves — the step closes without anyone clicking anything.
Through the API, GET /official/status tells the same story, live:
{ "connected": true, "can_send": true, "display_number": "+55 11 3333-3333",
"number_verified": true, "name_status": "APPROVED", "business_review": "APPROVED",
"quality_rating": "GREEN", "messaging_limit": "TIER_1K", "live": true }
can_send is the only question that matters. live: false means we could not reach Meta
right now and the values are the last known ones — the difference between "this is how it
is" and "we could not check" matters when you automate on top of it.
Several numbers in one project
Meta allows several numbers on one business account, and bZapper follows:
GET /official/numbers lists them all. Each has an instance_id — the same value you
pass in a send to choose which number the message goes out from, exactly as on the
unofficial rail.
Without instance_id, conversation affinity applies: it goes out from the number already
talking to that contact. With no prior conversation, from the project's default number.
To connect one more number from the same business account there is nothing to authorize
again — the authorization belongs to the account, not to the number.
GET /official/numbers/available lists the numbers the account holds at Meta that are not
in the project yet; POST /official/numbers with the chosen phone_number_id connects it.
We check at Meta that the number really belongs to that account before saving it, so a
wrong id fails there, not on the first send.
curl -X POST https://api.bzapper.com.br/official/numbers \
-H "Authorization: Bearer $BZAPPER_KEY" \
-H "Content-Type: application/json" \
-d '{ "phone_number_id": "106540352242922" }'
PUT /official/numbers/{id}/default changes the default number, and
DELETE /official/numbers/{id} disconnects one — its conversation history stays.
The 24-hour window
This is the rule that defines the official rail, and it is worth understanding once.
You may send free text to someone for 24 hours counted from the last message that person sent you. Outside that window, Meta refuses any free text. Only a template it has approved gets through.
This is not a bZapper limitation and there is no way around it: it is how the Cloud API is designed. What bZapper does is keep it from catching you by surprise.
The same endpoint works on both sides
Configure a fallback template on the project and you are done:
# once
curl -X PUT https://api.bzapper.com.br/templates/fallback \
-H "Authorization: Bearer $BZAPPER_KEY" \
-H "Content-Type: application/json" \
-d '{"template_id":"..."}'
From then on, the same POST /messages/text works inside and outside the window. When it
is closed, the message goes out as the template — and the response says what happened:
{
"message_id": "0f2c…",
"status": "queued",
"delivery": {
"via": "template_fallback",
"template": "reengajamento",
"note": "A janela de 24 horas com este contato estava fechada: a mensagem saiu pelo template de alternativa configurado no projeto."
}
}
The delivery field appears only when there was a detour. The platform decides for you,
but it does not hide what it decided — and note is a sentence ready to show to a person.
With no fallback configured, a send outside the window fails with 422
official_needs_template, telling you what to set up. We prefer that to handing back
Meta's 131047, which is what you would have without us.
Read and "typing…"
In an open conversation, two gestures make the business feel present:
POST /messages/{wa_message_id}/readmarks a received message as read: the contact sees the blue ✓✓ (earlier messages in the chat are marked too). Valid for 30 days.POST /presence/chatwithstate: "typing"shows "typing…" to the contact until the reply goes out or for 25 seconds. Meta ties the indicator to the contact's last message: with none, the answer is409 typing_needs_inbound. Use it only when you are actually replying.
Knowing whether the window is open
GET /conversations returns last_inbound_at on each conversation: the last message
from the contact. The window stays open until last_inbound_at + 24 hours. Without
the field, the contact never wrote — and only a template reaches them. It is the same data
the dashboard Inbox uses to warn the agent before sending.
Templates
A template is a message model approved by Meta. Approval is asynchronous and can take up to 24 hours; Meta rejects with a terse reason and caps how many you may submit.
That is why bZapper validates before sending — and it is where this rail earns its keep.
Creating one
You describe the message in a simple shape. We build the component format Meta expects, with its examples nested three levels deep and the rules it does not document alongside.
curl -X POST https://api.bzapper.com.br/templates \
-H "Authorization: Bearer $BZAPPER_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "pedido_confirmado",
"language": "pt_BR",
"category": "UTILITY",
"header": { "type": "text", "text": "Pedido {{1}}", "example": "10432" },
"body": {
"text": "Olá {{1}}, seu pedido {{2}} foi confirmado e já está em separação.",
"example": ["Ana", "10432"]
},
"footer": "Loja do Berni",
"buttons": [{ "type": "quick_reply", "text": "Acompanhar" }]
}'
An image, video or document header is also reviewed by Meta, from a sample file. You
do not need to host it anywhere: upload it with POST /templates/media (multipart, field
file — JPEG, PNG, MP4 or PDF up to 10 MB) and use the returned url as
header.media_url. It is only the sample Meta reviews, not the media that goes out on each
send.
curl -X POST https://api.bzapper.com.br/templates/media \
-H "Authorization: Bearer $BZAPPER_KEY" \
-F "[email protected]"
# { "url": "https://…/banner.jpg" } → "header": { "type": "image", "media_url": "https://…/banner.jpg" }
The rules that get you rejected (and that we block first)
Each of these comes back as 422 with a sentence saying what to fix, instead of becoming a Meta rejection 24 hours later:
- the body cannot start with
{{1}}— it needs text before it; - the body cannot end with a variable — it needs text after it;
- two variables side by side (
{{1}} {{2}}) do not pass; - numbering is
{{1}},{{2}},{{3}}— no gaps, starting at 1; - every variable needs an example (the most common rejection of all);
- the footer takes no variables, and is capped at 60 characters;
- a text header takes one variable, and is capped at 60 characters;
- at most 10 buttons, of which up to 2 links and 1 phone.
The category sets the price
MARKETING, UTILITY or AUTHENTICATION is not an administrative label: it is what
defines what the message costs and what Meta accepts in the content.
UTILITY is cheaper, but only covers what the customer already expects — a
confirmation, an order update, a reminder. Write "enjoy our discount" in a UTILITY template
and Meta reclassifies it on its own to MARKETING, and the price changes without you
noticing.
The creation response carries an advice field for exactly this kind of warning: things
that did not block the submission, but change the outcome.
Following the decision
You do not have to poll. The decision arrives by webhook:
| Event | When |
|---|---|
template.approved | Meta approved it; you can send. |
template.rejected | Rejected. The payload carries rejected_reason. |
template.paused | Paused for low quality — it stops sending. |
template.categorized | Meta reclassified it. The message price changes. |
template.quality_changed | Quality changed (GREEN, YELLOW, RED). |
Sending by name
curl -X POST https://api.bzapper.com.br/messages/template \
-H "Authorization: Bearer $BZAPPER_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "+5511999999999", "template": "pedido_confirmado",
"variables": ["Ana", "10432"], "header_variables": ["10432"] }'
The id Meta gave the template never appears. language may be omitted when the name
exists in a single language; if it exists in more than one, we answer 409
template_ambiguous rather than choosing for you — sending in the wrong language is
worse than asking.
The number of variables must match the approved template. We check before spending
the call: a mismatch is the number one cause of Meta error 132000.
Four details we handle for you:
- Named variables (
{{name}}): send the values in the order they appear in the body. We attach each variable's name, which Meta requires. - Image, video or document header: Meta requires the media on every send. Put the URL
in
header_variables[0]; omit it and the sample media from the template's creation goes out. - Copy-coupon button: Meta requires the coupon on every send. Put it in
button_variablesat the button's position; omit it and the coupon registered at creation goes out. With neither, the answer is422 template_coupon_required— before the call is spent. - Reply quoting a message:
quoted_message_id(thewa_message_id) makes the contact see the quoted message above the new one, on any message type.
Verification code (OTP)
In the AUTHENTICATION category Meta writes the text itself and adds the copy-code button.
You only set the validity:
{ "name": "codigo_acesso", "language": "pt_BR",
"category": "AUTHENTICATION", "code_expiry_minutes": 10 }
And you send with code instead of variables:
{ "to": "+5511999999999", "template": "codigo_acesso", "code": "738291" }
This is how you send an OTP outside the 24-hour window — which is the normal case for an access code, since the person has almost never written to you first.
Or let bZapper choose, through the same POST /messages/otp as the other rail:
{ "to": "+5511999999999", "code": "738291", "expiry_minutes": 10 }
- Window open: it goes as on the QR rail, text + the code alone in a bubble — no Meta charge (Meta bills authentication messages even inside the window).
- Window closed: it goes through the project's code template: the approved
authentication one (in the requested
language, if any), with the code in the body and in the copy button. The response tells the path indelivery:{ "via": "authentication_template", "template": "…" }. - Until Meta releases Authentication: the chosen template can be a provisional one:
Marketing with a copy-code button (the customer copies only the code;
"via": "marketing_template") or Utility with one variable that receives the code in the text ("via": "utility_template"). Marketing costs more and is not delivered to people who opted out of marketing. Meta may reject or recategorize that template, so it is a bridge, not the destination. "template": "codigo_acesso"in the request forces that template even with the window open; it must be approved and either authentication or the provisional one (422 official_otp_template_invalid).- Window closed and no code template:
422 official_otp_template_required. - Destination is a BSUID with no known phone:
422 official_otp_needs_phone— Meta does not deliver authentication templates to a BSUID. An OTP never goes through the fallback template — the code would have no place in it.
On both paths the code is never stored nor shown in the Inbox.
Which template carries the code — PUT /templates/otp with {"template_id": "…"}
chooses it (approved, either Authentication or a provisional one — Marketing with copy code,
or Utility with one variable — otherwise 422 template_not_otp_capable; empty clears it and the first approved
Authentication template is used again). GET /templates/otp returns the choice and the account's
eligibility, because Meta only allows the category once the business is verified and
the number reaches 2,000 new contacts a day:
{ "template": null,
"eligibility": { "business_verification": "pending", "verification_ok": false,
"messaging_limit": "TIER_250", "required_limit": "TIER_2K", "limit_ok": false,
"approved_auth_templates": 0 } }
Already have templates at Meta?
POST /templates/sync imports the ones that exist there and not here — the ones you
created in WhatsApp Manager before you met us — following every Meta page. For the ones
already here, it updates status, category and the text edited there. Without it they would
exist for Meta and be invisible in the panel, and sending by name would say they do not exist.
Edit
PUT /templates/{id} takes the same body as creation; name and language are the
template identity and do not change. The edit goes back to review (the template returns to
PENDING and does not send until the decision). For approved templates Meta allows 1 edit a
day and 10 a month and does not let the category change (422 template_category_locked); an
exceeded limit comes back as 422 meta_rejected, with Meta's sentence.
Validate without submitting
POST /templates/validate takes the same body as creation and answers
{ "valid": false, "problem": "…" } with what Meta would reject — with no call to Meta and no
submission spent (?id= checks an edit). It is what the panel uses while you type.
Performance by template
GET /templates/analytics?days=30 (1–90) counts, per template, what went out in the period:
sent, delivered, read, failed and the delivery_rate and read_rate (in %).
Location header
header.type = "location" creates the template with no sample. The map goes with each send,
in header_variables: [latitude, longitude, place name, address] (the last two optional).
Without latitude and longitude the send returns 422 saying so — before Meta.
Limited-time offer and carousel
Offer (MARKETING): limited_time_offer with the badge (up to 16 characters) and,
optionally, has_expiration. Needs a URL button; with a deadline, also the copy-code button.
No footer.
{ "name": "flash_offer", "language": "en_US", "category": "MARKETING",
"limited_time_offer": { "text": "Today only!", "has_expiration": true },
"body": { "text": "Hi {{1}}, your exclusive offer is waiting for you.", "example": ["Ana"] },
"buttons": [{ "type": "copy_code", "text": "Copy code", "example": "OFFER10" },
{ "type": "url", "text": "Shop now", "url": "https://store.com/offer" }] }
On send, offer_expires_at (RFC 3339) is the countdown deadline — without it, 422.
Carousel (MARKETING or UTILITY): cards with 2 to 10 cards; the template keeps only the
opening body. Each card has an image or video, text (up to 160 characters) and 1 or 2
buttons, with the same media type and buttons on all cards (Meta rule — we check first). On
send, cards carries the values of each card, in order:
{ "to": "+15551234567", "template": "showcase", "variables": ["Ana"],
"cards": [{ "variables": ["Runner"], "button_variables": ["10"] },
{ "header_url": "https://cdn.com/trail.jpg", "variables": ["Trail"], "button_variables": ["11"] }] }
Without header_url, the card sample media goes out.
Submission limit
Meta accepts up to 100 template submissions an hour per account. Beyond that, creating and
editing return 429 meta_rate_limited: try again in a few minutes.
Meta ready-made templates (instant approval)
Meta's Template Library holds utility and authentication texts it has already approved. Created without changing the text, the template is approved at once — no 24-hour review.
curl "https://api.bzapper.com.br/templates/library?search=order&language=en_US" \
-H "Authorization: Bearer $BZAPPER_KEY"
curl -X POST https://api.bzapper.com.br/templates/library \
-H "Authorization: Bearer $BZAPPER_KEY" -H "Content-Type: application/json" \
-d '{ "library_template_name": "order_confirmation_1", "name": "order_confirmation",
"language": "en_US",
"button_inputs": [{ "type": "url", "url": "https://yourstore.com/order/{{1}}",
"url_suffix_example": "123" }] }'
button_inputs gives, in order, what each link (url) or phone (phone_number, with
phone) button of the library template needs. Authentication items take
code_expiry_minutes (default 10).
Type parity
The 13 supportable types work the same on both rails, with the same request body: text, image, video, audio, document, sticker, location, contact, reaction, buttons, list, OTP and template.
Three differences worth noting:
Buttons and lists are better here. On the unofficial rail they are unstable and bZapper always falls back to a numbered text menu. On the Cloud API they are native and they render. Above Meta's limits (more than 3 buttons, a title over 20 characters, more than 10 list rows) we fall back to the same text menu — the message arrives, it just does not arrive clickable.
Poll does not exist. It is the only parity gap, and it is permanent: the Cloud API has
no such type. Sending a poll on an official project fails with 422
official_poll_unsupported and points at the alternative, which is /messages/list.
The destination is a phone number or a BSUID. The @lid belongs to multi-device
WhatsApp: the Cloud API neither addresses by it nor has the LID→phone map. A @lid in to
fails with 422 official_lid_unsupported — on the unofficial rail the same to works
(see Contacts with @lid).
BSUID: people who hide their phone
People who adopt a WhatsApp username can hide their phone. Meta then identifies them by the
BSUID (BR.13491208655302741918), which comes in every event — and, in those cases, is
the only address. bZapper stores the BSUID on the contact and returns it in the webhook as
sender.bsuid; a phone-less conversation has chat_jid <bsuid>@bsuid.
To reply, put the BSUID in to. If bZapper already knows that person's phone, it sends to
the phone; otherwise it sends by the BSUID. Two exceptions:
- Verification code: Meta does not deliver authentication templates to a BSUID. With no
known phone, the OTP fails with 422
official_otp_needs_phone. - Unofficial rail: the BSUID belongs to the official API; there it fails with 400
bsuid_official_only.
Pricing and origin in message webhooks
Two blocks exist only on the official rail and arrive in the payload of message.*
events.
pricing — what Meta will charge your customer. It comes in status events
(message.sent, message.delivered, message.read, message.failed) whenever Meta
includes the block in that status — in practice, message.delivered carries it:
"payload": {
"wa_message_id": "wamid.…",
"status": "delivered",
"pricing": { "category": "marketing", "billable": true, "type": "regular" }
}
category:marketing,utility,authentication,service… — the category Meta charged in, which may differ from the template's if Meta reclassified it.billable: whether the message is charged.type:regular,free_customer_service(a reply inside the window) orfree_entry_point(a conversation opened from an ad).
It is Meta's charge, on your customer's card; bZapper charges nothing per message and adds no margin. The field lets you add up spend per customer, campaign or template without opening Meta's dashboard.
referral — which ad the contact came from. When the conversation starts from a
click-to-WhatsApp ad or post, message.received carries the origin:
"payload": {
"type": "text",
"body": "Saw your ad",
"referral": {
"source_type": "ad", "source_id": "120200…", "source_url": "https://fb.me/…",
"headline": "50% off", "body": "Today only", "media_type": "image",
"image_url": "https://…", "ctwa_clid": "ARAk…"
}
}
source_id is the ad or post id; ctwa_clid is the click id, for sending the conversion
back to Ads Manager. Empty fields are omitted. The origin is stored with the message and
shows up in the conversation history.
What Meta will charge
GET /official/insights?days=30 returns the official-rail dashboard: the estimated cost
Meta will charge your customer (bZapper charges only the subscription) and the rail's KPIs.
{
"cost": {
"currency": "BRL", "period": 41.18, "previous_period": 36.02, "change_pct": 14.3,
"month_to_date": 27.40, "projected_month": 50.20,
"by_category": [
{ "category": "marketing", "messages": 120, "billable": 120, "cost": 38.60 },
{ "category": "utility", "messages": 90, "billable": 74, "cost": 2.58 },
{ "category": "authentication", "messages": 0, "billable": 0, "cost": 0 },
{ "category": "service", "messages": 310, "billable": 0, "cost": 0 }
],
"confirmed": 480, "estimated": 40, "other_countries": 0
},
"messages": { "sent": 520, "delivered": 507, "read": 431, "delivery_rate": 97.5, "read_rate": 82.9, "free_messages": 326 },
"templates": [ { "name": "promo_outubro", "category": "marketing", "sent": 120, "failed": 2, "cost": 38.60 } ],
"ad_referrals": 14, "marketing_opt_outs": 3
}
- Period:
days(ending now) orfrom/toin RFC 3339; up to one year. - The estimate uses the category and
billableflag Meta sends in each message's status (confirmed) and, when missing, the template's category (estimated, counting utility as charged). Service and utility inside the window are free. - Price follows the recipient's country;
rate_cardcarries the card used (Meta's for Brazil, in BRL, with the utility/authentication volume tiers). Other countries go tocost.other_countries, unpriced. projected_monthprojects the current month at the pace so far.
Before launching a campaign
GET /campaigns/meta-cost?recipients=5000&template=october_promo returns what Meta will
charge for the campaign (from the template category and the Brazil rate card) and, given the
number's daily new-contact limit, how many days it takes to reach everyone:
{ "currency": "BRL", "category": "marketing", "unit_price": 0.3217, "total": 1608.5,
"daily_limit": 1000, "days_needed": 5 }
Quality and limit history
GET /official/numbers/{id}/health returns the number's current state and every change of
quality_rating and messaging_limit (from Meta's webhook or a live check), newest first. A
repeated state is not history.
Account currency
GET /official/status includes currency, the currency Meta bills the account in. A
Brazilian account still in USD has to move to BRL — Meta set the deadline at 2027-06-30; until
then, BRL estimates are approximate.
Media stored at Meta
To send the same file to many people (a campaign image, a catalogue PDF), upload it
once and reuse the id:
curl -X POST "$BASE/official/media" -H "Authorization: Bearer $KEY" \
-F "[email protected];type=image/jpeg"
# → 201 { "id": "1234567890", "type": "image", "expires_at": "…", "instance_id": "…" }
Then, in any media send, "media": { "id": "1234567890", "caption": "…" } — Meta does not
download the file again for each message. A campaign variation accepts the same
media.id.
- The id is valid for 30 days and only for sends from the number that uploaded it
(
instance_idin the query picks which; without it, the project default). - Meta's limits, checked before uploading: image JPEG/PNG up to 5 MB, sticker WebP up to
500 KB, video MP4/3GP and audio AAC/AMR/MP3/MP4/OGG up to 16 MB, document PDF/Office/text
up to 64 MB. Otherwise
422 media_type_unsupportedor413 media_over_meta_limit, with both sizes in the message. GET /official/media/{id}checks it is still valid (404 media_not_foundonce expired);DELETEremoves it earlier.- On the unofficial rail
media.idanswers409 media_id_official_only: there, media goes byurlorbase64.
Contacts who stop marketing inside WhatsApp
A contact can tap "stop" on your business's marketing messages inside WhatsApp. bZapper gets this from Meta and:
- adds the contact to the suppression list with
reason: marketing_opt_outand emitscontact.opted_outwith{ "reason": "marketing_opt_out", "category": "marketing", "source": "meta:user_preferences" }; - refuses MARKETING templates to them with
422 contact_marketing_opted_out, before spending the call — campaigns included; - does not block the rest: replies inside the window and UTILITY or AUTHENTICATION templates still go out. Their request was about marketing.
When they resume, the suppression goes away on its own and contact.opted_in arrives
(same fields). A keyword opt-out (STOP) or a manual block is never undone by it. Meta's
131050 refusal (the contact had stopped before you connected) has the same effect.
When Meta refuses
bZapper turns its code into an instruction, keeping the number at the end for when you need to open a ticket with them:
| What Meta says | What you read |
|---|---|
131047 | The 24-hour window closed: use a template. |
131026 | WhatsApp could not deliver to this number. |
131049 | Meta chose not to deliver (contact saturated with marketing). |
131042 | Account eligibility — almost always the missing card. |
132000 | The number of variables does not match the template. |
132015 | Meta paused the template for low quality. |
130429 | The number hit its daily cap of started conversations. |
Two of these change the account state on their own: a refused eligibility marks the account as awaiting payment again (and the panel shows the step, with the instruction), and a policy block suspends it.
Business profile
The number's business card — what your customer sees when opening the chat: the short line under the name, description, address, e-mail, sector, up to two websites and the photo. All of it is edited from here, without opening WhatsApp Manager.
curl https://api.bzapper.com.br/official/profile -H "Authorization: Bearer $BZAPPER_KEY"
curl -X PUT https://api.bzapper.com.br/official/profile \
-H "Authorization: Bearer $BZAPPER_KEY" \
-H "Content-Type: application/json" \
-d '{ "about": "Open 8am to 6pm", "description": "Specialty coffee shop.",
"email": "[email protected]", "vertical": "RETAIL",
"websites": ["https://shop.com"] }'
At Meta an empty field does not mean "unchanged" — it means "clear it". Read it with
GET, change what you need and send everything back on PUT.
We check Meta's limits before sending — 139 characters for the short line, 512 for the description, 256 for the address, at most 2 websites and a closed list of sectors. When something overflows, the 422 names the field and by how much, instead of Meta's "Invalid parameter".
- Photo:
POST /official/profile/photo(multipart, fieldfile, JPEG or PNG up to 5 MB; Meta recommends 640×640). WhatsApp Business only has the round photo — there is no cover image. - Display name: the name change itself is requested in WhatsApp Manager and reviewed
by Meta. Once approved, the half that is ours remains:
POST /official/profile/reregister. Until the number is re-registered it keeps showing the old name — the step everybody forgets.
The profile belongs to the number, not to the account. With nothing in the path, the
routes above act on the default number; for another number use
/official/numbers/{id}/profile, /official/numbers/{id}/profile/photo and
/official/numbers/{id}/profile/reregister.
Quality and limit
Meta assigns each number a quality (GREEN, YELLOW, RED) and a daily cap on
started conversations (TIER_250, TIER_1K, …). The cap rises on its own with volume
and quality — and quality falls when people block or report you.
Both appear in the panel and in GET /official/status, updated by Meta's webhooks. You
never have to open WhatsApp Manager to see them.
An approved template is not a licence to blast. Quality at RED drops the cap, and a
dropped cap is dropped delivery. What protects the number is the same as on the other rail:
sending to people who want to hear from you. See
sending best practices.