Plans and reliability
Billing and Yanola Balance
Understand what the Plan and the account's shared Yanola Balance each pay for, and how usage is recorded.
What this guide covers
- Subscription access and spendable Yanola Balance are separate controls.
- Live API sends are included in the Plan up to the Message Cap.
- Test Mode and inbound webhook processing do not consume message-send balance.
Plan and Balance are separate
A Plan is the optional Developer Platform subscription of a Yanola Account: it grants API keys, webhooks, and API and SDK sends under a monthly Message Cap. Yanola Balance is prepaid USD that pays for SaaS usage across the account: workflows, scheduled sends, AI actions, functions, and storage. Paying for one does not fund or activate the other.
Without a Plan there is no API access, while the SaaS stays usable on the Yanola Balance. API and SDK sends never spend from the Balance, and workflow or scheduler sends never count against the Message Cap.
| Control | What it governs | What payment does not do |
|---|---|---|
Plan (Yanola Subscription) | Developer Platform access, Agent Project capacity, keys, webhooks, Message Cap, rate tier, retention, Test Mode, and support. | Does not add prepaid USD to Yanola Balance. |
Yanola Balance | Funds workflows, scheduled sends, AI actions, functions, storage, and other SaaS usage. | Does not start, renew, or restore a Plan, and does not raise the Message Cap. |
Subscription charges never spend from Yanola Balance. Balance top-ups never pay a subscription invoice.
Read the account-level Balance
Yanola Balance is shared across every Agent Project in the Yanola Account. The dashboard shows USD values for the amount currently available, lifetime purchased, lifetime used, promotional balance, and the configured low-balance and auto-top-up state.
availableAmount is the stored Balance minus any active holds. API and SDK sends create no hold.
| Value | Meaning |
|---|---|
availableAmount | Spendable USD after subtracting active usage reservations. |
lifetimePurchasedAmount | USD value added through completed purchases. |
lifetimeUsedAmount | Cumulative finalized usage deductions. |
lifetimeBonusAmount | Cumulative promotional value added to the Balance. |
lowBalanceThresholdAmount / isLow | The warning threshold and whether availableAmount is below it. |
autoTopUp | Whether automatic top-up is enabled, plus its saved amount and trigger threshold when configured. |
Billing contracts represent USD amounts as strings with two to six fractional digits. Preserve them as decimal strings or scaled integers instead of JavaScript Number values when doing accounting.
How a Live Mode API send is recorded
A Live Mode API or SDK send is included in the Plan: it carries no Yanola fee and no Meta Fee, and it counts against the monthly Message Cap. Its usage record is finalized when the send is accepted, with totalAmount 0.000000 and nothing deducted from Yanola Balance.
Meta bills your WhatsApp Business Account directly for the messages it charges for. That amount is not stored on the usage record, so providerAmount and yanolaFee are null on an included send.
{
"status": "finalized",
"currency": "USD",
"estimatedAmount": "0.000000",
"providerAmount": null,
"yanolaFee": null,
"totalAmount": "0.000000"
}| Field | Meaning |
|---|---|
providerAmount | null on an included send; Meta bills your WhatsApp Business Account directly. |
yanolaFee | null on an included send; API and SDK sends carry no Yanola fee. |
totalAmount | The Yanola Balance deduction: 0.000000 on an included send. |
providerCategory / market | Provider pricing context captured with the snapshot when available. |
estimatedAmount | 0.000000 on an included send, because nothing is held. |
GA pricing source and webhook billing
An API or SDK send within the Message Cap costs nothing beyond the Plan: totalAmount is 0.000000. The account Billing view and accepted subscription offer are authoritative for the current plan name, subscription price, Message Cap, and entitlement quantities; these commercial values are intentionally not REST or SDK constants.
A pricing or entitlement configuration change does not rewrite an existing usage record. Test Mode sends and inbound webhook processing do not consume Balance, and webhook deliveries are not separately billed in v1.
| Question | Authoritative source |
|---|---|
What does a Live Mode API send cost? | No Yanola fee and no Meta Fee: usage totalAmount is the USD decimal string 0.000000, and the send counts against the Message Cap. |
What are this account's current plan price and Message Cap? | The accepted subscription offer and account Billing view. |
What did an existing usage item cost? | Its finalized totalAmount. |
What are this account's capacity and retention limits? | The current entitlement plus REST rate-limit headers. |
Are v1 webhook deliveries billed separately? | No; they are controlled by entitlement, fair-use, replay, and abuse limits. |
Yanola can change commercial offers without changing /v1. Never copy development seed plan names, seed rates, or test entitlement values into production integration logic.
Live Mode billing lifecycle
Yanola performs the subscription, entitlement, Sender, and Message Cap checks before a send is queued, then creates its usage record at zero. A 202 response means the message was accepted, not that it was delivered. At the Message Cap a new send returns 402 plan_message_limit_reached, except replies inside the 24-hour Customer Service Window.
- Persist the accepted message ID and request ID.
- Observe message delivery state through retrieval, logs, or signed webhooks.
- Use client.usage.list(...) to correlate messageId with its usage record.
- Track accepted sends against the Message Cap in the dashboard; they do not spend Yanola Balance.
| Stage | Usage status | Balance effect |
|---|---|---|
Send accepted | finalized | None: totalAmount is 0.000000 and no hold is created. |
Delivery status received | finalized | None: the message status changes and the usage record stays as it is. |
Failed-message handling
A failed API or SDK send never spends Yanola Balance. Whatever Meta reports, its usage record stays at 0.000000. Meta decides on its own whether to bill your WhatsApp Business Account.
| Outcome | Billing decision | What to expect |
|---|---|---|
Yanola cannot enqueue the accepted operation before Meta processing | No Yanola charge | The send gives its place back in the Message Cap. |
Meta reports failed with billable: false or no billable evidence | No Yanola charge | The message status changes; usage stays at 0.000000. |
Meta reports failed with billable: true | No Yanola charge | Meta may bill your WhatsApp Business Account directly; nothing is deducted from Yanola Balance. |
A failed message can have finalized usage at 0.000000. Use the message status, not the usage record, to follow delivery.
Low and depleted Balance behavior
The Balance governs SaaS usage; API and SDK sends are never blocked by it. A low-balance warning is advisory: Balance-funded activity can still proceed when its cost fits within availableAmount, and it is blocked when it does not, even if the displayed Balance is above zero.
| State | Balance-funded activity | Access that remains |
|---|---|---|
Above the low threshold | Proceeds when its request checks pass. | Normal account and Developer Platform access. |
Below the low threshold but sufficient for the cost | Can proceed and further reduces availableAmount. | Normal access, with a low-balance warning. |
Insufficient for the cost or depleted | The activity does not run; a public API action billed from the Balance returns 402 insufficient_balance. | Dashboard, API sends within the Message Cap, API read and logs, and webhook configuration remain available when subscription access is active. |
Depleted with Test Mode included | Balance-funded activity stays blocked. | Synthetic Test Mode remains available and does not call Meta or consume Balance. |
Inbound webhook processing does not consume Yanola Balance. Workflows and other SaaS services triggered by an inbound message have their own Balance usage.
Handle insufficient_balance
API and SDK sends never return this code: at the Message Cap they return 402 plan_message_limit_reached. A public API action billed from the Balance that cannot be covered returns a non-retryable 402 billing_error. Log the request ID and structured details, top up the account, then retry the logical operation with the same application-owned idempotency key when applicable.
- Do not retry in a tight loop while the Balance is unchanged.
- Compare requiredAmount with availableAmount to choose an adequate top-up.
- Complete the Balance purchase and wait for the dashboard to show the added funds.
- Retry safely; do not create a new business operation merely because billing blocked the first attempt.
{
"error": {
"type": "billing_error",
"code": "insufficient_balance",
"message": "Yanola Balance is too low for this Live Mode usage reservation.",
"requestId": "req_...",
"retryable": false,
"details": {
"action": "top_up_balance",
"availableAmount": "0.100000",
"requiredAmount": "0.200000",
"currency": "USD"
}
}
}Top up Yanola Balance
Open the account Billing page or an Agent Project's Billing view and choose Top up balance. The current dashboard accepts a preset or custom amount from $10 through $10,000 and shows the amount paid, any promotional balance, Balance added, and Stripe-calculated tax before checkout.
A top-up is a one-time Stripe payment. Yanola adds the purchased and promotional USD value only after successful payment completion is received and applies repeated completion events at most once. Cancelled, failed, or merely opened checkout sessions do not add Balance.
- Choose an amount that covers the blocked action plus expected near-term usage.
- Review the estimate; tax is calculated during Stripe Checkout.
- Optionally enable auto top-up so the payment method can be saved for future off-session charges.
- Complete Stripe Checkout and return to Billing.
- Confirm availableAmount reflects the completed purchase before retrying blocked Live Mode work.
Top-ups fund the shared account Balance, not one Agent Project. Activity from any Agent Project can use the available funds.
Auto top-up behavior
Enable auto top-up during a manual Balance purchase. After that payment completes, Yanola stores the purchased amount as the automatic top-up amount and can charge the saved payment method off-session when available Balance reaches the configured trigger, currently 0.000000 USD.
Clearing the option during a later completed top-up turns auto top-up off. If an automatic payment fails, no Balance is added; update the payment method or complete a manual top-up before retrying blocked usage.
| Dashboard state | Meaning | Action |
|---|---|---|
Enabled | A saved amount and threshold are available for off-session charging. | Keep the payment method valid and monitor receipts. |
Needs attention | Auto top-up is enabled but its saved configuration is unavailable. | Complete a new top-up to refresh the configuration. |
Off | Yanola will not attempt an automatic Balance purchase. | Enable it during a manual top-up if desired. |
Reconcile and report usage
Use the account Billing views for the current Balance, purchases, adjustments, and invoices. Use the public usage API for per-message Developer Platform records; it is not a Balance ledger or subscription API.
- Group records by providerCategory and market when reviewing API volume; Meta invoices for your WhatsApp Business Account stay the source for Meta Fees.
- Expect totalAmount 0.000000 on included API and SDK sends.
- Keep any reserved or needs_reconciliation record visible as unsettled activity.
- Correlate billing questions with usage ID, message ID, and request ID.
import { WhatsAppClient } from "@yanola/whatsapp"
const apiKey = process.env.YANOLA_API_KEY
if (!apiKey) {
throw new Error("Missing YANOLA_API_KEY")
}
const yanola = new WhatsAppClient({ apiKey })
const [settled, unresolved] = await Promise.all([
yanola.usage.list({ status: "finalized", limit: 100 }),
yanola.usage.list({ status: "needs_reconciliation", limit: 100 }),
])
console.log({
settled: settled.data.map(({ messageId, totalAmount }) => ({
messageId,
totalAmount,
})),
unresolvedMessageIds: unresolved.data.map(({ messageId }) => messageId),
})