zhyper docs

Billing and balance

Most of what zhyper does costs us nothing per call. One thing does: X charges per API request, and that charge is passed through to you at cost.

zhyper is prepaid for that pass-through. You load a balance; metered publishes draw it down. There is no invoice at the end of the month for usage you have already spent, and therefore no bad debt and no surprise bill.

What is metered, and what is not

Exactly one platform is metered today: X. Every other platform your key can reach is not, and this asymmetry is the single most important thing on this page.

  • On X, a publish attempt that cannot be paid for does not happen. You get a terminal failure with code insufficient_balance, and the target's status is failed — not scheduled, not retried later. A prepaid call with no balance behind it is not a call we can make. The check prices the whole attempt up front, so a thread is weighed at its full cost before its first request leaves — see A thread is N billable requests below.
  • On every other platform, publishing is never blocked by balance. A zero balance changes nothing about Instagram, LinkedIn, Telegram or anything else.

If you read only one sentence: a drained balance stops X and nothing else.

You do not have to take this list on faith or keep it in sync by hand. GET /v1/billing returns both halves of it — blocksPublishOn and neverBlocksPublishOn — derived from the same source the publisher uses.

Reading your balance

GET /v1/billing needs accounts:read, the same permission as Platform capabilities and usage. A read-only key can call it.

It returns balanceMicros (integer) and balanceUsd (decimal string) for the calling team, plus insufficientBalance describing exactly what runs out of money means: which platforms it blocks, which it never blocks, whether the rule is currently enforced, and the error code you will see.

A team that has never loaded a balance gets 0, not a 404. The balance is the sum of its ledger, and the sum of an empty ledger is zero — the resource exists, its value is zero. The distinction between never funded and funded and spent to zero is carried by updatedAt, which is null in the first case.

The response deliberately carries no payment data — no card, no payment method, no invoice history, no customer id. Those are panel-only.

Money is integer micro-USD

Every amount in the API is an integer count of micro-USD: 1 USD = 1 000 000 micros. microsPerUnit states this in the response rather than leaving you to assume it.

This is not pedantry. X's cheapest metered operation costs $0.015 — below one cent — so a cents-based integer could not represent it at all, and floating point cannot represent it exactly either. Thousands of rows later that becomes a difference nobody can reconcile. Decimal strings are provided alongside (balanceUsd, unitCostUsd) for display; do your arithmetic on the integers.

X pricing is published, not implied

GET /v1/billing/x-pricing returns the per-operation price we charge you, machine-readable, with no markup. It is the same constant the publisher reads when it bills you — not a copy kept alongside it, which is exactly the kind of thing that drifts and turns into charging one price while advertising another.

Two tariffs apply today, and the gap between them is large:

TariffPrice
Post: Create$0.015
Post: Create (with URL)$0.200

That is more than a 13x difference for the same publish, decided by whether the text contains a link. A scheduling product sends links by nature, so this is not an edge case — it is most likely your normal cost.

A thread is N billable requests

X has no "post a thread" operation. A thread is posted the only way X allows: one request per item, each replying to the one before it. We bill what we are billed — a 5-item thread is 5 metered requests, not one.

Each item is priced on its own text. Putting a link in the last item of a 5-item thread raises that item to $0.200 and leaves the other four at $0.015; it does not reprice the thread. So a mixed thread produces more than one line on your ledger — one per tariff, carrying the number of requests at that tariff — and the sum of those lines is what the balance check weighed before the first request went out.

This is the single largest way to be surprised by a bill on this page, which is why it is stated here rather than left to be inferred from the tariff table.

X does not publish the rule it uses to decide what counts as a link, so our detector approximates it rather than copying it. We chose to make it broad: it will occasionally treat something as a link that X might not.

That direction is a deliberate trade, and we would rather state it than hide it. A narrow detector would undercharge and we would absorb the difference quietly; a broad one can overcharge — but visibly. The tariff actually applied is recorded on every billable event, and GET /v1/billing/x-pricing returns the rule and worked examples, so you can check our arithmetic and dispute it. A silent undercharge leaves no trace for anyone.

When a publish is billed

You are billed when the request reaches X, not when we decide to try.

  • A publish that X answers is billed, including when the answer is an error. A 400 or 403 still consumed the operation on X's side.
  • A publish that never reaches X is not billed: connection failures, and attempts abandoned before the call.
  • A timeout is not billed. X may or may not have processed it; the request is genuinely ambiguous, and we absorb the cost of that ambiguity rather than charging you for it.

A thread that breaks halfway is billed for the half that happened. If item 3 of 5 times out, the thread stops there and you are billed for the two requests X answered — not for five, and not for zero. The same rule as above, applied per request rather than per post.

The direction is one-way on purpose: the ledger may undercount, it will never overcount.

New card top-ups clear before they can be fully spent

A card payment can be reversed by the cardholder's bank weeks after it settles. So a top-up paid by card is spendable in full only after 7 days. Before that, we cap how much of it can be turned into X requests.

The rule, in one line:

Your total X spend to date may not exceed everything that has cleared, minus refunds, plus $250.

Cleared means everything that is not a card top-up from the last 7 days: older card payments, bank transfers, and operator credits all count immediately. Note this is measured against your lifetime spend, not your current balance — so in practice it only ever bites in the days right after a card payment, and only past the first $250.

A worked example. You are new, you pay $250 by card, and you spend it the same afternoon: nothing stops you, because the first $250 is the allowance. Later, with $250 already cleared and $250 of it spent, you top up $2,000 by card: you can spend $250 more right away, and the rest once that payment clears.

A publish stopped by this rule is a terminal failure with code unsettled_funds_hold. It is deliberately not insufficient_balance: your balance is real and you can see it. The two codes tell you to do different things — one means load money, this one means wait, or pay by bank transfer.

There is nothing to do to "clear" a payment faster; the window is measured from when the payment landed, and it runs whether or not you publish.

Loading a balance

POST /v1/billing/checkout (createBillingCheckout) opens a Stripe Checkout session and returns the one-time url to send the customer to, together with the session id and the amount in both scales (amountMicros, amountUsd). Four things about it are worth knowing before you build against it:

  • It is panel-only. An API key cannot call it, at all — 403 actor_not_allowed. billing:manage is a control-plane permission and no key carries it, and the endpoint additionally refuses any non-session caller. Inside the panel it is open to owner, admin and billing_admin (Authentication).
  • It does not move your balance. The balance rises only when the payment completes and Stripe's signed webhook lands. Calling it twice opens two sessions; you are credited for the ones you actually pay.
  • The amount is a decimal USD string"25", "49.50" — with at most two decimal places, because Stripe charges in whole cents. More than two is 400 amount_sub_cent, not a silent rounding. Whitespace, a currency symbol, a thousands separator and scientific notation are all rejected.
  • The first top-up has a lower ceiling than later ones. Until a team has completed one successful top-up, a single transaction is capped below the general maximum, and exceeding that cap is its own code, 400 amount_above_first_topup_cap, so your UI can say "complete a smaller one first" rather than "make it smaller". Both ceilings are carried in the amountUsd field description of CreateBillingCheckoutDto in the OpenAPI document (Quickstart has the two URLs). They are not restated here, and the API reference does not print them either: it lists each operation's body fields, not their descriptions. Read the amounts from the document, because a copy on this page would be the next thing to go stale.

Whether the endpoint exists at all is a property of the deployment. Stripe is behind a flag, and when it is off the endpoint answers 404 — not 503, not a friendlier error, because a configuration that does not exist should not have a reachable endpoint. On a deployment where it is off, balances are loaded by us: contact support and we will credit the team and give you the reference. Write your top-up flow so that a 404 here means "this deployment does not do self-service top-up", and not "something broke".

See also

On this page