BFA NotifyDocumentation
Console

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, read or failed.
  • 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.

The API is versioned in the path. v1 is stable — we add fields, we do not remove or rename them.
Base URL
https://notify.bfa.zone/api.php/v1
cURL
# 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.

  1. You get a sending number. Rent one from the catalogue in the console. Every message you send comes from it.
  2. You write templates. WhatsApp requires pre-approved wording to start a conversation. You submit them here, Meta reviews them.
  3. You send — by hand from the console, as a bulk campaign, on a schedule, or from your own code through the API.
  4. WhatsApp reports back. Each message moves through sentdeliveredread, 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.

You are billed only for messages WhatsApp accepts. Anything it refuses — a wrong number, a blocked template — is refunded to your balance automatically, and shows as a refund in your usage history.
Nothing here requires a developer. The console does everything; the API exists for when you want your own system to send automatically.

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.

Checklist
□  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
Sending fails with “no phone number on this account” until step 2 is done. That is the most common first-day question.

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.

Switching the sender changes which templates you can use, because templates belong to the WhatsApp account behind the number. The list updates as you switch.
Customers who have not saved your number see the digits, not your business name, in the chat header. That is a WhatsApp rule for accounts without the Official Business Account badge — your name still shows on your profile.

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.
  • CategoryUtility (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

StatusMeaning
DraftWaiting for platform approval before it reaches Meta.
PendingWith Meta for review.
ApprovedReady to send.
RejectedMeta 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.

Example body
Hi {{1}}, your order {{2}} is on its way
and should arrive by {{3}}.

Thank you for shopping with us.
Keep one template per purpose and reuse it. A short, approved library beats many near-duplicates, and Meta rates your account on how people react to what you send.

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.

CSV for bulk
phone,name,order,eta
+9647500000001,Ahmed,ORD-1024,17:30
+9647500000002,Sara,ORD-1025,18:00

# then map:  {{1}} → name   {{2}} → order   {{3}} → eta
If your spreadsheet shows a number as 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

ItemPrice
Utility or Authentication template25 IQD per message
Marketing template55 IQD per message
Free text inside the 24-hour windowFree
A rented numberIts 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.

Messages stuck at sent and never reaching delivered usually mean the recipient's phone is offline — WhatsApp keeps trying for up to 30 days.
Keep a little balance spare. A bulk campaign is checked against the whole batch before it starts, and a rented number is released if its renewal cannot be paid.

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.
Repeatedly trying to send with a blocked or unapproved template can suspend your account automatically. If a template is refused, fix it rather than retrying.
Asking permission
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 & APIDeveloper APIGenerate 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
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"]
      }'
PHP
<?php
$ch = curl_init('https://notify.bfa.zone/api.php/v1/messages');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer wa_your_key_here',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'to'        => '+9647500000000',
        'template'  => 'order_shipped',
        'language'  => 'en',
        'variables' => ['Ahmed', 'ORD-1024'],
    ]),
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
Node.js
// Node 18+ (built-in fetch)
const res = await fetch('https://notify.bfa.zone/api.php/v1/messages', {
    method: 'POST',
    headers: {
        Authorization: 'Bearer wa_your_key_here',
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        to: '+9647500000000',
        template: 'order_shipped',
        language: 'en',
        variables: ['Ahmed', 'ORD-1024'],
    }),
});
console.log(await res.json());
Python
import requests

res = requests.post(
    "https://notify.bfa.zone/api.php/v1/messages",
    headers={
        "Authorization": "Bearer wa_your_key_here",
        "Content-Type": "application/json",
    },
    json={
        "to": "+9647500000000",
        "template": "order_shipped",
        "language": "en",
        "variables": ["Ahmed", "ORD-1024"],
    },
)
print(res.json())
Java
// Java 11+ (java.net.http)
HttpClient client = HttpClient.newHttpClient();

String json = "{\"to\":\"+9647500000000\",\"template\":\"order_shipped\","
            + "\"language\":\"en\",\"variables\":[\"Ahmed\",\"ORD-1024\"]}";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://notify.bfa.zone/api.php/v1/messages"))
    .header("Authorization", "Bearer wa_your_key_here")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(json))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
C#
// .NET 6+
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer wa_your_key_here");

var json = "{\"to\":\"+9647500000000\",\"template\":\"order_shipped\","
         + "\"language\":\"en\",\"variables\":[\"Ahmed\",\"ORD-1024\"]}";
var content = new StringContent(json, Encoding.UTF8, "application/json");

var response = await client.PostAsync(
    "https://notify.bfa.zone/api.php/v1/messages", content);
Console.WriteLine(await response.Content.ReadAsStringAsync());
Response
{
  "ok": true,
  "message_id": 4821,
  "status": "queued",
  "to": "+9647500000000",
  "error": null
}

Authentication #

Send your key as a bearer token on every request:

HeaderValue
AuthorizationBearer wa_your_key_here
Content-Typeapplication/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.

Keep the key on your server. Anyone holding it can send messages and spend your balance. Never ship it in a mobile app or browser JavaScript. If it leaks, press Regenerate in the console — the old key dies instantly.
Missing key
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.

No number
HTTP 402
{
  "error": "No phone number on this account — rent one in the console under Phone Numbers."
}

Send a text message #

POST/v1/messages

Plain text reaches anyone who has messaged you in the last 24 hours — WhatsApp calls this the service window. Outside it, WhatsApp only accepts templates.

FieldTypeNotes
to requiredstring Recipient in international format, e.g. +9647500000000. Always include the country code — a local number without one may be refused by WhatsApp.
body requiredstring The message text.
name optionalstring A label for your own logs; the recipient never sees it.
Free-text replies inside the service window are free — they cost 0 IQD from your balance.
cURL
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."}'
PHP
$payload = [
    'to'   => '+9647500000000',
    'body' => 'Thanks! Your order ships today.',
];
// same curl_setopt_array call as the quick start
Node.js
await send({ to: '+9647500000000', body: 'Thanks! Your order ships today.' });
Python
send({"to": "+9647500000000", "body": "Thanks! Your order ships today."})
Response
{
  "ok": true,
  "message_id": 4822,
  "status": "queued",
  "to": "+9647500000000",
  "error": null
}

Send a template #

POST/v1/messages

Templates 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.

FieldTypeNotes
to requiredstringRecipient.
template requiredstring Template name exactly as approved, e.g. order_shipped.
language optionalstring Language code of the approved template. Defaults to en. Must match — en and en_US are different templates.
variables optionalstring[] Fills {{1}}, {{2}}… in order. Give every placeholder a value.
button_variables optionalstring[] Values for dynamic URL buttons, e.g. an order id appended to a tracking link.
header_value conditionalstring 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.
Restricted numbers. Numbers on the account's restricted list (console → Bulk MessagingRestricted list) are never messaged. A single send to one returns 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.
Use GET /v1/templates to see exactly which names, languages and placeholder counts your account can send.
cURL
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"]
      }'
PHP
$payload = [
    'to'               => '+9647500000000',
    'template'         => 'booking_reminder',
    'language'         => 'en',
    'variables'        => ['Ahmed', '30/6', '5:30PM'],
    'button_variables' => ['ORD-1024'],
];
Node.js
await send({
    to: '+9647500000000',
    template: 'booking_reminder',
    language: 'en',
    variables: ['Ahmed', '30/6', '5:30PM'],
    button_variables: ['ORD-1024'],
});
Python
send({
    "to": "+9647500000000",
    "template": "booking_reminder",
    "language": "en",
    "variables": ["Ahmed", "30/6", "5:30PM"],
    "button_variables": ["ORD-1024"],
})
Wrong account
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 #

POST/v1/messages/bulk

One 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.

FieldTypeNotes
to either this…string[] A plain list of numbers. A comma-separated string works too.
recipients …or thisobject[] Each item takes to, and optionally variables, button_variables, header_value and name.
variables optionalstring[] Shared values for every recipient that does not bring its own.
header_value conditionalstring 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 recommendedstring Template name. Without one, body is sent as free text and only reaches people inside the 24-hour window.
language optionalstringDefaults to en.
name optionalstring Campaign label; groups the run in Logs and Reports.
Your balance must cover the whole batch before it starts, otherwise the call is rejected with 402 and nothing is sent.
cURL
# 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"]}
        ]
      }'
PHP
$payload = [
    'name'       => 'Friday reminders',
    'template'   => 'booking_reminder',
    'language'   => 'en',
    'recipients' => [
        ['to' => '+9647500000001', 'variables' => ['Ahmed', '30/6', '5:30PM']],
        ['to' => '+9647500000002', 'variables' => ['Sara', '1/7', '6:00PM']],
    ],
];
Node.js
await bulk({
    name: 'Friday reminders',
    template: 'booking_reminder',
    language: 'en',
    recipients: [
        { to: '+9647500000001', variables: ['Ahmed', '30/6', '5:30PM'] },
        { to: '+9647500000002', variables: ['Sara', '1/7', '6:00PM'] },
    ],
});
Python
bulk({
    "name": "Friday reminders",
    "template": "booking_reminder",
    "language": "en",
    "recipients": [
        {"to": "+9647500000001", "variables": ["Ahmed", "30/6", "5:30PM"]},
        {"to": "+9647500000002", "variables": ["Sara", "1/7", "6:00PM"]},
    ],
})
Response
{
  "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 #

GET/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.

Statuses only ever move forward — a late delivered callback can never downgrade a message already marked read.
cURL
curl https://notify.bfa.zone/api.php/v1/messages/4822 \
  -H "Authorization: Bearer wa_your_key_here"
Response
{
  "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 #

GET/v1/me

Who 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.

GET/v1/balance

What is left and what a message costs, so you can refuse a campaign in your own system before it starts.

GET/v1/balance/history

Charges, top-ups and refunds, newest first, each with the balance after it. ?limit= up to 200.

GET/v1/reports

Totals for a date range — by status, by day and by template. ?from=YYYY-MM-DD&to=YYYY-MM-DD, defaulting to this month.

GET/v1/campaigns

Bulk runs with their delivered/failed split and what each cost.

GET /v1/me
{
  "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 }
  }
}
GET /v1/balance
{
  "balance": 53225,
  "currency": "IQD",
  "rates": { "utility": 25, "authentication": 25, "marketing": 55, "service": 0 }
}
GET /v1/reports
{
  "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 #

GET/v1/messages

Everything you have sent, newest first — the same data as the console's Logs page.

QueryNotes
limit1–200, default 50.
statusOne of the statuses.
sinceDate or datetime; only newer messages.
before_idPaging: pass the next_before_id from the last page.
Polling this with since every few minutes is the simplest way to mirror delivery states into your own database while webhooks do not exist yet.
Response
{
  "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 #

GET/v1/contacts

Your saved contacts. Filter with ?q= (name or number) and ?tag=; ?limit= up to 500.

POST/v1/contacts

Add 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.

PUT/v1/contacts/{id}

Update any subset of name, phone, tag, fields.

DELETE/v1/contacts/{id}

Remove it. Messages already sent to that person stay in your log.

cURL
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" }
      }'
Python
requests.post(
    "https://notify.bfa.zone/api.php/v1/contacts",
    headers={"Authorization": "Bearer wa_your_key_here",
             "Content-Type": "application/json"},
    json={"name": "Ahmed Ali", "phone": "+9647500000001",
          "tag": "Customer", "fields": {"city": "Erbil"}},
)
Node.js
await fetch('https://notify.bfa.zone/api.php/v1/contacts', {
    method: 'POST',
    headers: {
        Authorization: 'Bearer wa_your_key_here',
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        name: 'Ahmed Ali', phone: '+9647500000001',
        tag: 'Customer', fields: { city: 'Erbil' },
    }),
});
Response
{
  "ok": true,
  "id": 149,
  "contact": {
    "name": "Ahmed Ali",
    "phone": "+9647500000001",
    "tag": "Customer",
    "fields": { "city": "Erbil", "order": "ORD-1024" }
  }
}

Scheduling #

POST/v1/schedules

Queue 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.

FieldNotes
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.
GET/v1/schedules

Everything queued, sent or cancelled, newest first.

DELETE/v1/schedules/{id}

Cancel one that has not run yet. Anything already sent returns 404.

Schedule a send
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"]
        }
      }'
Response
{ "ok": true, "id": 12, "send_at": "2026-08-01 09:30:00", "status": "pending" }

Open service windows #

GET/v1/windows

Who 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.

Response
{
  "open": [
    { "phone": "+9647500000001",
      "last_message_at": "2026-07-26 09:12:40",
      "closes_in_minutes": 903 }
  ],
  "window_hours": 24
}

Status reference #

StatusMeaningBilled
queuedAccepted by us, on its way to WhatsApp.Reserved
sentWhatsApp accepted it and is delivering.Yes
deliveredIt reached the recipient's phone.Yes
readThe recipient opened it.Yes
failedWhatsApp refused or could not deliver it — error_detail says why.Refunded
no_whatsappThat number has no WhatsApp account. Stop retrying it.Refunded

Templates #

GET/v1/templates

Every template your account can send, with its language, category and body — so you can build the variable list without guessing.

FieldNotes
nameUse this exact value as template.
langPass as language. The same name may exist in several languages.
categoryUtility, Marketing or Authentication — sets the price.
statusOnly Approved templates can be sent.
bodyThe approved text, with {{1}} placeholders.
url_buttonsHow many button_variables the template expects.
header_formatNONE, TEXT, IMAGE, VIDEO or DOCUMENT. Anything but the first two means the send needs a header_value.
header_textThe header wording for a TEXT header. A {{1}} in it is filled with header_value.
header_mediaThe default file saved for this template in the console, used whenever header_value is omitted.
cURL
curl https://notify.bfa.zone/api.php/v1/templates \
  -H "Authorization: Bearer wa_your_key_here"
Response
{
  "templates": [
    {
      "name": "order_shipped",
      "lang": "en",
      "category": "Utility",
      "status": "Approved",
      "body": "Hi {{1}}, order {{2}} is on its way.",
      "url_buttons": 0
    }
  ]
}

Numbers #

GET/v1/numbers

The WhatsApp numbers your business currently holds. The one flagged default: true is what API sends go out from.

FieldNotes
phone_number_idMeta's id for the number.
display_phoneThe number as customers see it.
waba_idThe WhatsApp account it belongs to — templates are scoped to this.
statusconnected when it is ready to send.
defaulttrue for the number API sends use.
Response
{
  "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.

MessagePrice
Utility template25 IQD
Authentication template25 IQD
Marketing template55 IQD
Free text inside the 24-hour service windowFree

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.

Out of balance
HTTP 402
{ "error": "Insufficient balance" }
A refund lands back on your balance within seconds of WhatsApp reporting the failure, and appears in the console's usage history.

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.

CodeMeaningWhat to do
401Missing or invalid API key.Check the Authorization header.
402No balance, or no number on the account.Top up, or rent a number.
403Account suspended, or the template is blocked.Contact support.
404Unknown endpoint or message id.Check the path and the id.
422Something in the body is wrong — bad number, missing field, template on another account.Read error; it names the field.
500Our side failed.Retry shortly; nothing was charged.
A 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 #

LimitValue
Recipients per bulk call1,000
Messages per daySet by WhatsApp per number (tier), not by us — visible in the console under Phone Numbers.
Free textOnly within 24 hours of the customer's last message.
Template variablesEvery {{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.

Open the console →