BFA Notify documentation
Send WhatsApp messages from your own website, CRM, shop or backend — order updates, appointment reminders, delivery notices — with one HTTP request. Everything you can do in the console you can do from code.
What you get
- One endpoint to send — plain text or an approved WhatsApp template.
- Delivery tracking — every message gets an id you can poll for
sent,delivered,readorfailed. - Bulk in one call — up to 1,000 recipients, each with their own variables.
- Honest billing — a message Meta refuses is refunded to your balance automatically. Free-text replies inside the 24-hour service window cost nothing.
Where to go next
Running your messaging by hand? Start with How it works and Set up your account — no code involved. Wiring up your own system? Jump to the developer quick start.
Base URL
Every API endpoint hangs off this address. All requests are HTTPS and all bodies are JSON.
https://notify.bfa.zone/api.php/v1
# the shortest possible send
curl -X POST https://notify.bfa.zone/api.php/v1/messages \
-H "Authorization: Bearer wa_your_key_here" \
-H "Content-Type: application/json" \
-d '{"to":"+9647500000000","body":"Your order is on its way."}'
How it works #
BFA Notify sits between your business and WhatsApp. You never deal with Meta directly — we hold the WhatsApp connection, you hold an account with a balance.
- You get a sending number. Rent one from the catalogue in the console. Every message you send comes from it.
- You write templates. WhatsApp requires pre-approved wording to start a conversation. You submit them here, Meta reviews them.
- You send — by hand from the console, as a bulk campaign, on a schedule, or from your own code through the API.
- WhatsApp reports back. Each message moves through
sent→delivered→read, and your balance is charged per message.
Two rules worth knowing up front
The 24-hour window. Once a customer messages you, you can reply with any free text for 24 hours, and those replies are free. Outside that window WhatsApp only accepts an approved template.
Templates belong to one WhatsApp account. If you hold numbers on two accounts, each has its own template list — the console only offers the ones your chosen sender can actually use.
Set up your account #
Six steps, in order. Each one is a page in the console's left menu.
1. Sign in
Use the email, username or user ID you were given, or continue with Google or Facebook. Your user ID works as a login on its own — it is on Profile & API.
2. Rent a phone number
Phone Numbers lists what is available with a price per 30 days. Renting charges your balance and the number works immediately. Without one you cannot send at all.
3. Add a template
Templates → write one, or copy a ready-made one from Template Library. Meta usually reviews within minutes to a few hours.
4. Top up your balance
Balance → top up with FIB or another method offered in your console. The full amount you pay is credited; we add no top-up fee.
5. Send a test to yourself
Send Message → your own number → pick the template → send. Then check
Logs to watch it turn delivered.
6. Optional: connect your systems
Profile & API → generate an API key and follow the developer quick start.
□ Signed in □ A phone number rented and showing "Ready to send" □ At least one template Approved □ Balance topped up □ Test message delivered to my own phone □ (optional) API key generated and stored on my server
Phone numbers #
Phone Numbers in the console has two parts: the numbers you hold, and the ones you can take.
Renting
Each available number shows a title, a short description and a price per 30 days. Renting charges your balance straight away and the number becomes a sender you can pick on the Send page. Some numbers are shared — a card shows how many places are left; some are offered only to particular businesses, or only to brand-new accounts as a free welcome number.
Renewal
A rented number renews every 30 days from your balance on its renewal date. Switch auto-renew off to stop at the end of the current term — you keep it until then. If your balance is short on the renewal date, the number is released.
Releasing
Release hands it back at once. Days already paid for are not refunded, and the number returns to the available list.
Numbers given to you
The platform can also hand you a number directly — it shows as Free, with an end date if one was set. Nothing is charged for those.
Default sender
Hold more than one and a Send from selector appears on the Send and Bulk pages. The number marked Default sender is used by the API and whenever you do not choose.
Templates & approval #
A template is a message Meta has approved in advance. It is the only way to message someone who has not written to you in the last 24 hours.
Writing one
Templates → new. You choose:
- Name — lowercase letters, digits and underscores, e.g.
order_shipped. - Category — Utility (order and account updates), Marketing (offers, re-engagement) or Authentication (one-time codes). The category sets the price.
- Language — one template per language; the same name can exist in several.
- Body — your text with
{{1}},{{2}}where values change. - Footer and a link button — optional. A button link can end in
{{1}}so each message tracks a different order.
Statuses
| Status | Meaning |
|---|---|
| Draft | Waiting for platform approval before it reaches Meta. |
| Pending | With Meta for review. |
| Approved | Ready to send. |
| Rejected | Meta refused it — edit the wording and submit again. |
Getting approved first time
- Write it as a real message to a real customer, not a slogan.
- Pick the honest category. Marketing wording in a Utility template is the most common rejection.
- No placeholder-only lines:
{{1}}alone as the whole message is refused. - Never promise what you cannot do, and do not imply WhatsApp endorses you.
Template Library
Ready-made templates for common cases — order confirmations, delivery updates, appointment reminders. Import one and it goes through the same review.
Hi {{1}}, your order {{2}} is on its way
and should arrive by {{3}}.
Thank you for shopping with us.
Sending from the console #
One message
Send Message → type or pick a recipient, choose the sender if you hold
more than one number, choose a template and fill its values. The preview on the
right shows exactly what the customer will see. Local numbers like
07XXXXXXXXX are converted to international format for you.
Bulk campaigns
Bulk Messaging sends one template to many people, personalised per
recipient. Recipients come from your saved contacts, a pasted list, or a CSV
upload. With a CSV you map each {{1}} to a column, so every message
carries that row's own values. Name the campaign and it is grouped in Logs and
Reports.
Scheduling
Both pages have Schedule — pick a date and time and it is queued. Scheduled lists everything pending and lets you cancel. Scheduled sends run even with your browser closed, and are charged when they actually go out.
Contacts
Contacts stores names, numbers, tags and custom fields. Tags let you send to a group; custom fields fill template values automatically. You can import a CSV and export at any time.
phone,name,order,eta
+9647500000001,Ahmed,ORD-1024,17:30
+9647500000002,Sara,ORD-1025,18:00
# then map: {{1}} → name {{2}} → order {{3}} → eta
9.64751E+12, format that
column as text before exporting, or the digits are lost.Logs, reports & balance #
Logs
Every message with its status, cost, template and who sent it. Filter by status, by team member or by date, save a filter you use often, and export to CSV or Excel. A failed message shows WhatsApp's own reason.
Reports
Totals by day, by template and by team member — delivered, read, failed, and what you spent. Useful for judging which template actually gets read.
Balance
The pill at the top right is your balance. Top Up takes payment and credits the full amount — we charge no top-up fee. Usage history lists every charge, top-up and refund with a running balance, so any figure can be traced.
What things cost
| Item | Price |
|---|---|
| Utility or Authentication template | 25 IQD per message |
| Marketing template | 55 IQD per message |
| Free text inside the 24-hour window | Free |
| A rented number | Its listed price, every 30 days |
Team & roles
Team & Roles invites colleagues and controls what each can do — sending, bulk, templates, contacts, logs, renting numbers, seeing the balance. Staff see only the messages they sent themselves. You can never grant a permission you do not hold.
sent and never reaching
delivered usually mean the recipient's phone is offline — WhatsApp
keeps trying for up to 30 days.Staying in good standing #
WhatsApp scores every business on how people react to its messages. A good score raises how many people you may message a day; a bad one gets the number limited. Everything below protects that score.
- Message people who expect it. Ask permission when you take the number, and say what you will send.
- Send useful things. Order updates and reminders get read. Repeated promotions get blocked.
- Honour opt-outs immediately. If someone asks you to stop, remove them from your contacts and never send again.
- Do not buy number lists. It is the fastest way to lose a number.
- Keep one message per purpose and do not send the same thing twice.
- Watch quality under Phone Numbers — a rating dropping to yellow or red is a warning before limits arrive.
At checkout, next to the phone field: "Send me order updates on WhatsApp" [x] Keep the record of that tick. It is your answer if a customer ever complains.
Developer quick start #
Four steps from nothing to a delivered message.
1. Get your API key
Open the console → Profile & API → Developer API → Generate API key.
Keys start with wa_ followed by 32 characters. Regenerating a key disables the old
one immediately.
2. Make sure you hold a number
Messages leave from a WhatsApp number your business holds. Rent one under Phone Numbers in the console — see Before you can send.
3. Send
Pick your language on the right. Anyone who has messaged you in the last 24 hours can receive plain text; everyone else needs an approved template.
4. Track it
Keep the message_id from the response and poll
GET /v1/messages/{id} for the delivery state.
curl -X POST https://notify.bfa.zone/api.php/v1/messages \
-H "Authorization: Bearer wa_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"to": "+9647500000000",
"template": "order_shipped",
"language": "en",
"variables": ["Ahmed", "ORD-1024"]
}'
{
"ok": true,
"message_id": 4821,
"status": "queued",
"to": "+9647500000000",
"error": null
}
Authentication #
Send your key as a bearer token on every request:
| Header | Value |
|---|---|
Authorization | Bearer wa_your_key_here |
Content-Type | application/json (on POST) |
The key identifies your business, so it also decides which numbers, templates and balance the call uses. There is nothing else to configure.
HTTP 401
{ "error": "Missing API key. Send header: Authorization: Bearer <key>" }
HTTP 401
{ "error": "Invalid API key" }
HTTP 403
{ "error": "This account is suspended" }
Before you can send #
Messages go out from a WhatsApp number your business holds. You get one by renting it in the console under Phone Numbers, or by the platform owner assigning one to you. Until then the API answers:
Calls use your default sender — the number marked default in
GET /v1/numbers. Change it in the console.
Templates belong to a number
A WhatsApp template lives on the account (WABA) that owns your number, so
GET /v1/templates and every send are scoped to it.
Sending a template that belongs elsewhere returns a clear 422.
HTTP 402
{
"error": "No phone number on this account — rent one in the console under Phone Numbers."
}
Send a text message #
/v1/messagesPlain text reaches anyone who has messaged you in the last 24 hours — WhatsApp calls this the service window. Outside it, WhatsApp only accepts templates.
| Field | Type | Notes |
|---|---|---|
to required | string | Recipient in international format, e.g. +9647500000000.
Always include the country code — a local number without one may be
refused by WhatsApp. |
body required | string | The message text. |
name optional | string | A label for your own logs; the recipient never sees it. |
curl -X POST https://notify.bfa.zone/api.php/v1/messages \
-H "Authorization: Bearer wa_your_key_here" \
-H "Content-Type: application/json" \
-d '{"to":"+9647500000000","body":"Thanks! Your order ships today."}'
{
"ok": true,
"message_id": 4822,
"status": "queued",
"to": "+9647500000000",
"error": null
}
Send a template #
/v1/messagesTemplates are pre-approved messages with {{1}}, {{2}}
placeholders. They are the only way to start a conversation — no service window
needed. Create and sync them in the console under Templates.
| Field | Type | Notes |
|---|---|---|
to required | string | Recipient. |
template required | string | Template name exactly as approved, e.g. order_shipped. |
language optional | string | Language code of the approved template. Defaults to en.
Must match — en and en_US are different templates. |
variables optional | string[] | Fills {{1}}, {{2}}… in order. Give every
placeholder a value. |
button_variables optional | string[] | Values for dynamic URL buttons, e.g. an order id appended to a tracking link. |
header_value conditional | string | Required when the template opens with an image, video or document —
WhatsApp stores the template, never the file. Pass a public
https link, or a media id already uploaded to Meta. Omit it to use
the default saved for that template in the console. A text header containing
{{1}} takes its value here too. |
422; in a bulk call that recipient
comes back with "error": "Number is on the restricted list" and is
not charged. Matching ignores formatting, so 0770… and
+964770… are the same number.GET /v1/templates to see exactly which
names, languages and placeholder counts your account can send.curl -X POST https://notify.bfa.zone/api.php/v1/messages \
-H "Authorization: Bearer wa_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"to": "+9647500000000",
"template": "booking_reminder",
"language": "en",
"variables": ["Ahmed", "30/6", "5:30PM"],
"button_variables": ["ORD-1024"]
}'
HTTP 422
{
"error": "\"autumn_promo\" belongs to the WhatsApp account \"Second line\".
Send it from a number on that account, or pick a template from the
account this number belongs to."
}
Send to many recipients #
/v1/messages/bulkOne template to many people in a single call, up to 1,000 recipients. The response reports every recipient separately, so a bad number never sinks the batch.
Many numbers, one set of values
The usual case: pass to as a list of numbers and variables
once — every recipient gets the same values.
Different values per person
Pass recipients instead, each with its own variables.
You can mix the two: a recipient without variables falls back to the
shared set.
| Field | Type | Notes |
|---|---|---|
to either this… | string[] | A plain list of numbers. A comma-separated string works too. |
recipients …or this | object[] | Each item takes to, and optionally variables,
button_variables, header_value and name. |
variables optional | string[] | Shared values for every recipient that does not bring its own. |
header_value conditional | string | The image / video / document link for a template that opens with one, shared by the whole batch. A recipient may carry its own to override it. |
template recommended | string | Template name. Without one, body is sent as free text and only
reaches people inside the 24-hour window. |
language optional | string | Defaults to en. |
name optional | string | Campaign label; groups the run in Logs and Reports. |
402 and nothing is sent.# many numbers, one shared variable
curl -X POST https://notify.bfa.zone/api.php/v1/messages/bulk \
-H "Authorization: Bearer wa_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Weekend offer",
"template": "weekend_offer",
"language": "en",
"variables": ["Ramadan Sale"],
"to": ["+9647500000001", "+9647500000002", "+9647500000003"]
}'
# or a different set of values per person
curl -X POST https://notify.bfa.zone/api.php/v1/messages/bulk \
-H "Authorization: Bearer wa_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"template": "booking_reminder",
"language": "en",
"recipients": [
{"to":"+9647500000001","variables":["Ahmed","30/6","5:30PM"]},
{"to":"+9647500000002","variables":["Sara","1/7","6:00PM"]}
]
}'
{
"ok": true,
"campaign_id": 312,
"total": 2,
"sent": 1,
"failed": 1,
"results": [
{ "to": "+9647500000001", "ok": true, "message_id": 4830, "status": "sent", "error": null },
{ "to": "+9647500000002", "ok": false, "message_id": 4831, "status": "failed",
"error": "Receiver is not a valid WhatsApp user" }
]
}
Message status #
/v1/messages/{id}Sending returns immediately, so delivery is reported afterwards. Poll this
endpoint with the message_id you were given.
How often to poll
Most messages reach sent within a second or two and
delivered within seconds when the handset is online. A sensible
pattern is to check after 5 seconds, then every 30 seconds for a few minutes,
then give up and rely on the console's Logs page.
delivered callback can
never downgrade a message already marked read.curl https://notify.bfa.zone/api.php/v1/messages/4822 \ -H "Authorization: Bearer wa_your_key_here"
{
"message": {
"id": 4822,
"recipient_phone": "+9647500000000",
"status": "delivered",
"msg_type": "api",
"template_name": "order_shipped",
"error_detail": null,
"cost": "25.0000",
"created_at": "2026-07-26 11:20:04"
}
}
Account, balance & reports #
/v1/meWho the key belongs to, and whether it can send right now. Call it on start-up:
can_send: false tells you why before a single message fails.
/v1/balanceWhat is left and what a message costs, so you can refuse a campaign in your own system before it starts.
/v1/balance/historyCharges, top-ups and refunds, newest first, each with the balance after it.
?limit= up to 200.
/v1/reportsTotals for a date range — by status, by day and by template.
?from=YYYY-MM-DD&to=YYYY-MM-DD, defaulting to this month.
/v1/campaignsBulk runs with their delivered/failed split and what each cost.
{
"account": {
"name": "Your Business",
"status": "active",
"balance": 53225,
"currency": "IQD",
"numbers": 3,
"can_send": true,
"blocked_reason": null,
"default_sender": "100000000000001",
"counts": { "contacts": 18, "templates": 13, "messages": 230 }
}
}
{
"balance": 53225,
"currency": "IQD",
"rates": { "utility": 25, "authentication": 25, "marketing": 55, "service": 0 }
}
{
"from": "2026-07-01",
"to": "2026-07-31",
"total": 29,
"spent": 550,
"by_status": { "read": 20, "sent": 2, "failed": 6, "no_whatsapp": 1 },
"daily": [ { "day": "2026-07-18", "n": 4, "cost": "100.0000" } ],
"by_template": [ { "template_name": "order_shipped", "n": 12 } ]
}
The message log #
/v1/messagesEverything you have sent, newest first — the same data as the console's Logs page.
| Query | Notes |
|---|---|
limit | 1–200, default 50. |
status | One of the statuses. |
since | Date or datetime; only newer messages. |
before_id | Paging: pass the next_before_id from the last page. |
since every few minutes is the simplest way to
mirror delivery states into your own database while webhooks do not exist yet.{
"messages": [
{
"id": 4830,
"recipient_phone": "+9647500000001",
"recipient_name": null,
"status": "read",
"msg_type": "api",
"template_name": "order_shipped",
"body": "Hi Ahmed, order ORD-1024 is on its way.",
"error_detail": null,
"cost": "25.0000",
"from_pnid": "100000000000001",
"campaign_id": null,
"created_at": "2026-07-26 11:20:04"
}
],
"next_before_id": 4830
}
Contacts #
/v1/contactsYour saved contacts. Filter with ?q= (name or number) and
?tag=; ?limit= up to 500.
/v1/contactsAdd one. name and phone are required; tag
defaults to Lead. fields takes up to 20 of your own
key/value pairs, which bulk sends can use to fill template variables.
A number that already exists returns 409 with the existing id.
/v1/contacts/{id}Update any subset of name, phone, tag,
fields.
/v1/contacts/{id}Remove it. Messages already sent to that person stay in your log.
curl -X POST https://notify.bfa.zone/api.php/v1/contacts \
-H "Authorization: Bearer wa_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Ahmed Ali",
"phone": "+9647500000001",
"tag": "Customer",
"fields": { "city": "Erbil", "order": "ORD-1024" }
}'
{
"ok": true,
"id": 149,
"contact": {
"name": "Ahmed Ali",
"phone": "+9647500000001",
"tag": "Customer",
"fields": { "city": "Erbil", "order": "ORD-1024" }
}
}
Scheduling #
/v1/schedulesQueue a send for later. payload is exactly the body you would post to
/v1/messages — or to
/v1/messages/bulk with
"kind": "bulk". It runs on our servers, so nothing needs to stay
online at your end, and it is charged when it actually goes out.
| Field | Notes |
|---|---|
send_at required |
When to send, e.g. 2026-08-01 09:30. Must be in the future. |
payload required |
The message, exactly as the send endpoints take it. |
kind optional |
send (default) or bulk. |
label optional |
Shown in the console's Scheduled page. |
/v1/schedulesEverything queued, sent or cancelled, newest first.
/v1/schedules/{id}Cancel one that has not run yet. Anything already sent returns 404.
curl -X POST https://notify.bfa.zone/api.php/v1/schedules \
-H "Authorization: Bearer wa_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"send_at": "2026-08-01 09:30",
"kind": "send",
"label": "Monday reminder",
"payload": {
"to": "+9647500000001",
"template": "booking_reminder",
"language": "en",
"variables": ["Ahmed", "1/8", "09:30"]
}
}'
{ "ok": true, "id": 12, "send_at": "2026-08-01 09:30:00", "status": "pending" }
Open service windows #
/v1/windowsWho you may send free text to right now, because they messaged you within the last 24 hours — and how long each window has left.
Use it to decide, per recipient, whether to send a cheap free-text reply or a paid template. Anyone not in this list needs a template.
{
"open": [
{ "phone": "+9647500000001",
"last_message_at": "2026-07-26 09:12:40",
"closes_in_minutes": 903 }
],
"window_hours": 24
}
Status reference #
| Status | Meaning | Billed |
|---|---|---|
queued | Accepted by us, on its way to WhatsApp. | Reserved |
sent | WhatsApp accepted it and is delivering. | Yes |
delivered | It reached the recipient's phone. | Yes |
read | The recipient opened it. | Yes |
failed | WhatsApp refused or could not deliver it —
error_detail says why. | Refunded |
no_whatsapp | That number has no WhatsApp account. Stop retrying it. | Refunded |
Templates #
/v1/templatesEvery template your account can send, with its language, category and body — so you can build the variable list without guessing.
| Field | Notes |
|---|---|
name | Use this exact value as template. |
lang | Pass as language. The same name may exist in several languages. |
category | Utility, Marketing or Authentication — sets the price. |
status | Only Approved templates can be sent. |
body | The approved text, with {{1}} placeholders. |
url_buttons | How many button_variables the template expects. |
header_format | NONE, TEXT, IMAGE,
VIDEO or DOCUMENT. Anything but the first two means the send
needs a header_value. |
header_text | The header wording for a TEXT header. A
{{1}} in it is filled with header_value. |
header_media | The default file saved for this template in the
console, used whenever header_value is omitted. |
curl https://notify.bfa.zone/api.php/v1/templates \ -H "Authorization: Bearer wa_your_key_here"
{
"templates": [
{
"name": "order_shipped",
"lang": "en",
"category": "Utility",
"status": "Approved",
"body": "Hi {{1}}, order {{2}} is on its way.",
"url_buttons": 0
}
]
}
Numbers #
/v1/numbersThe WhatsApp numbers your business currently holds. The one flagged
default: true is what API sends go out from.
| Field | Notes |
|---|---|
phone_number_id | Meta's id for the number. |
display_phone | The number as customers see it. |
waba_id | The WhatsApp account it belongs to — templates are scoped to this. |
status | connected when it is ready to send. |
default | true for the number API sends use. |
{
"numbers": [
{
"phone_number_id": "100000000000001",
"waba_id": "200000000000001",
"display_phone": "+964 750 000 0000",
"status": "connected",
"default": true
}
]
}
Pricing & billing #
Every message is charged to your account balance the moment WhatsApp accepts it. Anything WhatsApp refuses is refunded automatically — you are never billed for a message that did not go out.
| Message | Price |
|---|---|
| Utility template | 25 IQD |
| Authentication template | 25 IQD |
| Marketing template | 55 IQD |
| Free text inside the 24-hour service window | Free |
The template's own category sets the price, so check
category before a big campaign. Top up in the
console under Balance; renting a number is billed separately every 30 days.
HTTP 402
{ "error": "Insufficient balance" }
Errors #
Errors use standard HTTP status codes and always return the same shape:
{ "error": "what went wrong" }. The text is written to be shown to a
human, so you can log or surface it directly.
| Code | Meaning | What to do |
|---|---|---|
401 | Missing or invalid API key. | Check the Authorization header. |
402 | No balance, or no number on the account. | Top up, or rent a number. |
403 | Account suspended, or the template is blocked. | Contact support. |
404 | Unknown endpoint or message id. | Check the path and the id. |
422 | Something in the body is wrong — bad number, missing field, template on another account. | Read error; it names the field. |
500 | Our side failed. | Retry shortly; nothing was charged. |
200 with "status": "failed" is not an API error — it
means we accepted the request and WhatsApp then refused the message.
error_detail carries WhatsApp's own reason.Limits #
| Limit | Value |
|---|---|
| Recipients per bulk call | 1,000 |
| Messages per day | Set by WhatsApp per number (tier), not by us — visible in the console under Phone Numbers. |
| Free text | Only within 24 hours of the customer's last message. |
| Template variables | Every {{n}} in the body must get a value. |
Send messages one request at a time per recipient; there is no need to open dozens of parallel connections, and bulk exists precisely so you do not have to.
FAQ #
Why does the recipient see my number instead of my business name?
WhatsApp only shows a business name in the chat header for accounts with the Official Business Account badge. Your display name still appears on your profile. Ask us about applying for the badge.
Can I send images, PDFs or buttons?
Not through v1 yet — text and templates (including templates with URL buttons) are supported today. Tell support what you need and we will prioritise it.
Do you push delivery updates to my server?
Not yet. Poll GET /v1/messages/{id} for now;
outbound webhooks are on the roadmap.
Is there a sandbox?
Send to your own number first — it costs one message and exercises the exact production path.
What happens if I send the same message twice?
It is delivered twice and billed twice; there is no automatic de-duplication. Keep your own record of what you have sent.
Support #
Open a ticket from the console under Support — it reaches us with your
account attached, which is the fastest way to get an answer. Include the
message_id when you are asking about a specific message.