8. API keys

Create keys on the panel's Developer API page (/api). The plaintext is shown once — store it. Each key has an optional per-day cap (max_per_day; 0 = server default).

Action Where
Create / list / pause / delete panel /api page (session-authenticated)
Verify a key + read balance GET https://mp.dvocorp.com/api/v1/me/ping with the key

Signing in to the panel (to get that session)

Two doors, one account: email + password, or Continue with Google. A Google sign-in creates the ordinary account (same starter bonuses, same plans, same keys); signing in with Google using an address that already has an account takes you into that account rather than making a second one.

Action Endpoint
Password sign-in POST https://mp.dvocorp.com/api/v1/auth/login {email, password, captcha_token?}
Start a Google sign-in POST https://mp.dvocorp.com/api/v1/auth/google/start {next, mode} → {authorization_url}
Finish it (browser returns with ?code=&state=) POST https://mp.dvocorp.com/api/v1/auth/google/callback {code, state} → {access_token, created}
Set / change your own password POST https://mp.dvocorp.com/api/v1/auth/password {current_password?, new_password}
Attach / detach Google POST / DELETE https://mp.dvocorp.com/api/v1/auth/google/link
Register with an email POST https://mp.dvocorp.com/api/v1/auth/register {email, password, full_name?, captcha_token?}
Is this address usable? POST https://mp.dvocorp.com/api/v1/auth/email/check {email} → {acceptable, verdict}
Send / resend the confirmation code POST https://mp.dvocorp.com/api/v1/auth/email/send-code
Confirm with the code POST https://mp.dvocorp.com/api/v1/auth/email/verify {code} → {email_verified, granted_credits}

Confirming your email

Registration works with any mailbox, not just Gmail. The account is created and fully usable immediately — studio, API keys, uploads, everything. What waits for confirmation is the welcome bonuses.

Nothing is emailed by /auth/register. The 6-digit code is sent when you ask for it (/auth/email/send-code, which is what the banner's Confirm button calls) — a message sent during signup is one that ages out unread. Entering the code adds the welcome bonuses to your balance and subscribes you to job notifications and product news. /auth/email/status reports code_pending (one is already in the inbox, so don't send another) and resend_available_in (seconds until the next send is allowed).

Signing in with Google skips this — Google already verified the address, so the bonuses are granted right away.

Because open signup is the one endpoint that costs us money per request, it is guarded: a Cloudflare Turnstile captcha (when configured — sign-in carries the same check, since that is where leaked-password lists get tried), a per-IP rate limit, an address check (shape + does the domain accept mail + no disposable inboxes), and hard limits on how often a code can be sent or guessed. verdict from /auth/email/check is one of ok | invalid | no_domain | disposable | unknown (unknown = our DNS could not answer, and the address is still accepted).

The captcha's public site key, whether confirmation is available and what it pays are all in GET https://mp.dvocorp.com/api/v1/public/config (turnstile_site_key, email_verification_enabled, verify_bonus_credits).

Bonus history lives at GET https://mp.dvocorp.com/api/v1/billing/ledger?limit=&offset=&entry_type= (panel session; entry_type: purchase | debit | refund | signup_bonus | adjust | subscription | expire) — the same data the /ledger page shows.

Your orders are a different list, at GET https://mp.dvocorp.com/api/v1/billing/payments?limit=&offset= (panel session), newest first. The ledger records every movement of your balance; this records what you paid for. They are not interchangeable: a plan term is one order followed by a monthly grant, so reading receipts off the ledger would print twelve invoices for one charge. Each row carries order_id (the number to quote to support), kind (package | subscription), item_name, credits (on a plan order, the whole term, not the monthly allowance), months, extends_term, amount_cents / currency, actually_paid_cents, status and credited_at. Orders that never completed are returned too — if you paid and saw nothing, the order_id is here.

Plans & bonuses

Everything below needs a panel session (the access_token from /auth/login), not an API key — buying is a browser flow. To read your balance with a key, use GET https://mp.dvocorp.com/api/v1/me/ping instead.

Naming. The panel calls the unit bonuses. Nothing is renamed in the API: it is still credits on a balance, credits_per_call on an operation, period_credits on a plan, and the 402 body still reads not enough credits. Same numbers, same field names — only the word a human reads changed.

# 0. the public price list — no account, no key, never cached
curl https://mp.dvocorp.com/api/v1/public/pricing
# -> {"packages":[{"id":1,"name":"Starter","credits":500,"price_cents":500,
#      "charge_cents":500,"reference_cents":null,"badge":null,
#      "is_popular":false,"currency":"USD"}],"plans":[…],"tax_included":false}

The response is the price list; the line above is only its shape. Packs and plans are edited in the admin panel, so any figure written into this document is stale the moment someone changes one — read the amounts from the call, never from here. Prices are pre-tax: sales tax or VAT is added at checkout by the reseller, based on where the buyer is. The endpoint reads the same rows the checkout charges from, so the quoted figure and the charged figure cannot drift.

# 1. what you can buy — prices are already personalised for YOUR account
curl -H "Authorization: Bearer $JWT" https://mp.dvocorp.com/api/v1/billing/plans
# -> [{"id":2,"slug":"pro","name":"Pro","price_cents":2900,"currency":"USD",
#      "period_credits":6000,"period_days":30,"priority":5,
#      "is_locked_price":false,"list_price_cents":2900,
#      "terms":[{"months":1,"credits_per_month":6000,"credits_total":6000,
#                "monthly_price_cents":2900,"undiscounted_cents":2900,
#                "charge_cents":2900,"discount_pct":0,"currency":"USD"}]}]
#    Shape, not a quote — the amounts come from the call, not from this page.

price_cents and period_credits are per month. terms is the list of term lengths you may buy in one payment; it always has at least the one-month entry, and charge_cents on the entry you pick is exactly what you will be charged. Do not compute a multi-month price yourself — undiscounted_cents is the struck-through reference, charge_cents is the bill, and discount_pct is derived from the two. terms also appears on the anonymous GET /public/pricing plans, at list price.

Promotions: what the extra fields on a pack or a plan mean

Both GET /public/pricing and GET /billing/plans carry these. All of them are resolved by the server; none of them is something you compute.

Field On Means
charge_cents pack, plan The amount that will actually be taken. On a plan this is the monthly figure with the tier's promo applied; price_cents beside it is the undiscounted monthly rate
reference_cents pack, plan The struck-through "before" price, or null
badge pack, plan A short key, not a sentence — recommended, new, limited, or absent. Render your own wording for it
is_popular pack, plan This row is the most bought over the trailing window. Computed from paid orders; nobody can set it
promo_ends_at pack, plan When the discount stops, if it has an end. A date, ISO-8601

reference_cents has three states and they are not the same thing.

Two reasons that matters if you are rendering our prices anywhere: the percentage has to be calculated from the 30-day reference rather than from the last regular price (CJEU C-330/23, Aldi Süd, 26 Sep 2024), and a "reduced" price that equals or exceeds the prior one may not be presented as a reduction at all (same judgment, para. 27).

A multi-month term uses a different reference and the two must not be mixed on one line. A term is struck through against undiscounted_cents — your own monthly rate × the months — because it is a tied offer ("this rate when you buy three months"), not a price that fell over time. reference_cents belongs to the monthly price only.

Discounts compose by multiplication. A 20% plan promo and a 25% twelve-month term is 40% off, not 45%. You never have to do that arithmetic — charge_cents and discount_pct are both already computed — but if you are reconciling our numbers against your own, that is the rule.

# 2. how you can pay — one entry means there is no choice to make
curl -H "Authorization: Bearer $JWT" https://mp.dvocorp.com/api/v1/billing/providers
# -> [{"name":"liqpay","label":"LiqPay","kind":"card","is_default":true},
#     {"name":"coinbase","label":"Coinbase Commerce","kind":"crypto","is_default":false}]
# 3. start a plan purchase -> open `redirect_url` in a browser to pay
#    "provider" is optional: omit it for the default rail above
#    "months" is optional and defaults to 1 — one of the `months` values in
#    `terms` above; anything else is refused with 400
curl -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
  -d '{"plan_id":2,"months":6,"provider":"coinbase",
       "accept_documents":true,"accept_immediate":true,"consent_locale":"en"}' \
  https://mp.dvocorp.com/api/v1/billing/subscribe
# -> {"order_id":"ord_...","redirect_url":"https://commerce.coinbase.com/charges/...",
#     "status":"waiting","provider":"coinbase","balance":100}

One endpoint, two outcomes, and the server picks. Post it with no term running and you open a new one. Post it for the same plan while a term is running and you extend that term — the months are added to the end and nothing about the month you are already in changes. Post it for a different plan while a term is running and you get 409: there is no proration here, and refusing is the only answer that cannot destroy time you already paid for. Contact support to change tier; they can refund and re-sell.

# 4. or buy a one-off bonus pack (no monthly reset on these)
#    on this deployment a pack is an ADD-ON: you need a running plan term to
#    buy one — see below, and check `purchasable` before you post
curl -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
  -d '{"package_id":1,"provider":"liqpay",
       "accept_documents":true,"accept_immediate":true,"consent_locale":"en"}' \
  https://mp.dvocorp.com/api/v1/billing/checkout
# 4b. or buy BOTH IN ONE ORDER — a term and a pack, one charge, the pack
#     discounted for being bought together. Quote it first, then post the same
#     `package_id` to /subscribe; there is no second checkout and no second
#     payment.
curl -H "Authorization: Bearer $JWT" \
  "https://mp.dvocorp.com/api/v1/billing/bundle-quote?plan_id=2&package_id=1&months=6"
# -> {"plan_id":2,"package_id":1,"months":6,
#     "term":{"months":6,"credits_per_month":6000,"credits_total":36000,
#             "monthly_price_cents":2900,"undiscounted_cents":17400,
#             "charge_cents":14700,"discount_pct":15,"currency":"USD"},
#     "pack_credits":500,"pack_list_cents":500,"pack_charge_cents":425,
#     "pack_discount_pct":15,"total_cents":15125,"currency":"USD"}
#    Shape, not a quote — the amounts come from the call, not from this page.

curl -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
  -d '{"plan_id":2,"months":6,"package_id":1,
       "accept_documents":true,"accept_immediate":true,"consent_locale":"en"}' \
  https://mp.dvocorp.com/api/v1/billing/subscribe
# -> {"order_id":"ord_...","status":"waiting","provider":"liqpay","balance":100}

One order, one charge, two line items. total_cents is what the rail takes and it is the only figure you should show a customer — do not add term.charge_cents and pack_charge_cents yourself, and do not apply a percentage to anything. The quote comes from the same function the checkout charges from, so the number you were quoted and the number you are debited cannot differ.

The discount applies to the PACK only. The term keeps whatever its own length already earns it (term.discount_pct, from the term ladder) and the bundle percentage comes off the pack (pack_discount_pct). The two compose; neither is applied twice. pack_list_cents is what the same pack costs bought on its own — a real price you could be charged — which is why it is the honest thing to strike through. It is a conditional offer: post POST /billing/checkout for that pack alone and you are charged pack_list_cents, so any UI showing the lower figure has to say what the condition is.

A bundle is not refused by the pack rule. The rule below asks for a running term; in a bundle the term is in the same order, which is exactly the case it has to allow — so an account with no subscription at all can buy one. A pack on its own is still refused.

Settlement delivers both, exactly once. One payment opens (or extends) the term and grants the pack in the same transaction. The pack's bonuses land in full and carry expires_at: null — they never expire, and buying them beside a term changed the price and nothing else. The term's allowance still arrives one month at a time and each month's remainder still lapses when the next lands; a combined balance figure must never blur the two. GET /billing/payments returns the pack half as bundle_item_name / bundle_credits / bundle_amount_cents / bundle_discount_pct, separate from credits and amount_cents, for the same reason.

GET /public/config → billing.bundle_pack_discount_pct is the rule (the percentage, published for anyone); every pack row on GET /billing/packages and GET /public/pricing also carries bundle_charge_cents, so a price list can show the bundled figure before a plan is chosen without computing anything. 0 means the owner has switched the incentive off — a pack can still be added to a term, at its ordinary price.

TWO CONSENTS, AND BOTH ARE REQUIRED. Every purchase call carries two booleans, and a checkout missing either is refused with 400 naming the one that is missing. They are separate fields because one tick cannot lawfully carry both: express consent to immediate performance takes a positive act of its own, and accepting the general terms and conditions does not supply it (European Commission Notice 2021/C 525/01, §5.7 on the Consumer Rights Directive). The single accept_terms field the old contract used is gone, not aliased — a synonym that satisfied both would rebuild the defect silently.

field what it means
accept_documents the buyer accepts the Terms of Service and the Refund Policy
accept_immediate the buyer expressly asks you to begin providing the service at once, and acknowledges that bonuses already spent on completed jobs cannot be handed back
consent_locale optional; which translation of those two sentences was on screen (en, uk, ru). Stored on the order so the confirmation can quote them back in the language they were read in. Anything else is stored as "unrecorded"
terms_version optional and ignored. The server stamps its own published version — a value you supply identifies nothing

Send true for either one only when a person actually ticked a box that said so, unticked, in front of them. accept_immediate does not waive the right of withdrawal and must never be presented as if it did: buying a unit-denominated balance is not the supply of digital content, so unspent bonuses stay refundable.

400 {"detail":"tick every box before paying — still missing: accept_immediate (your request that we begin providing the service immediately)"}

You do not have to build the receipt. When an order is delivered — the moment the bonuses land — we email the buyer a confirmation of the concluded contract: the order id and date, the line items (both halves of a bundle, and for a plan the term length and the fact that the allowance arrives monthly), the amount and currency, the two consent sentences they ticked quoted verbatim with the documents version, their right of withdrawal and how to exercise it, and links to the Terms, the Refund Policy and their invoices. It is sent once per order, in the language recorded on the order (English otherwise), and it is not sent again on a repeated provider callback.

A pack may need a plan. When the operator has switched packs to add-ons, POST /billing/checkout refuses an account with no active subscription:

409 {"detail":{"code":"subscription_required",
               "message":"a one-off pack needs an active subscription — start a monthly plan first"}}

The detail is an object, not a string, so code is the field to branch on — it is what separates this from a failed payment. Nothing is opened by the refusal: there is no order to poll and nothing to cancel. Buy a term with POST /billing/subscribe (plans are never gated — that is the way in) and the packs open up.

Two ways to know before you post, and both come from the server:

This gates buying, never spending. Packs you have already bought keep expires_at: null and stay spendable for ever — after a term lapses, after it is cancelled, and whatever this setting is later changed to. Only the plan's own monthly allowance is swept at the end of a month; see the table below.

# 5. your balance, and how much of it renews
curl -H "Authorization: Bearer $JWT" https://mp.dvocorp.com/api/v1/billing/balance
# -> {"balance":6100,"expiring":6000,"expires_at":"2026-09-03T10:00:00Z"}
# 6. the term you hold — or null
curl -H "Authorization: Bearer $JWT" https://mp.dvocorp.com/api/v1/billing/subscription
# -> {"plan_name":"Pro","period_start":"2026-08-29T10:00:00Z",
#     "period_end":"2027-02-25T10:00:00Z","credits_per_month":6000,
#     "months_total":6,"months_granted":1,
#     "next_grant_at":"2026-09-28T10:00:00Z","can_extend":true,
#     "auto_renew":false,"renewal_status":"off","renews_at":null,
#     "renewal_amount_cents":0,"renewal_months":0,
#     "cancel_at_period_end":false,"grace_until":null,"can_cancel":false}

Two dates, and they mean different things. next_grant_at is when your next monthly allowance lands (and when the current one is void); period_end is when access ends. On a multi-month term they are far apart — a countdown that shows one of them is wrong about the other. Both are server timestamps; compute from them, not from a client clock plus a duration. next_grant_at is null when every allowance has been delivered and the term is simply running out.

credits_per_period is the old name for credits_per_month and carries the same number; it will be dropped a release from now.

How bonuses behave. You have one balance, and up to four kinds of bonus can be sitting in it:

Bonus Ledger entry_type Expires
Daily free allowance daily_free ~24h after it lands
Welcome grant (for confirming your email) signup_bonus 90 days after it is paid
Plan bonuses subscription at the end of each month of the term
Packs you buy, and anything an administrator grants you purchase, adjust never, while your account is open — a lapsed or cancelled plan does not touch them

A charge always consumes the soonest-expiring bonus first. A bonus that never expires is therefore always spent last: nothing you paid for is lost to a reset, and nothing you were given is left to evaporate while you still had a use for it. Where two kinds lapse at the same moment, they are spent in the order of the table above. Note that a monthly plan runs out before a 90-day welcome grant, so on a monthly term the plan bonuses go first — the rule is the expiry date, not the row order.

A plan pays out one monthly allowance at a time. Buying three, six or twelve months buys that many allowances — not one large balance. Each month's allowance is void the moment the next one lands: the remainder is replaced, not added to it. The last one is void when the term ends. So a 12-month Pro customer holds 6 000 bonuses at a time, twelve times over; never 72 000 at once.

expiring in the balance response is the part that lapses on expires_at; it is not a separate wallet. On a multi-month term expires_at is the next monthly boundary, not the end of the term — GET /billing/subscription has both dates.

The free tier is a daily allowance, not a plan. There is no free plan to sign up for and no feature is switched off without one. An eligible account is topped up with a small number of bonuses once a day, usable on anything the API offers, and the FIRST allowance lands the moment the account is created — you never wait on a scheduled pass to make your first call. Whether an account with an unconfirmed address is eligible is a setting, so if your balance is zero right after signing up, confirm your address. Like plan bonuses they are refreshed rather than accumulated — yesterday's unused allowance is replaced by today's, not added to it, so the daily figure is also the most you can hold. Confirming your email grants a larger one-off amount on top (granted_credits in the confirm response), and that welcome grant lapses 90 days after it is paid — it is a promotional bonus, not a purchase. Ledger entries are typed daily_free, and the top-up stops while a paid plan is active, since a plan already grants its own allowance.

Practical consequence for API clients: the free tier is fine for steady low-volume work and cannot do bursts. If you need to process a batch in one go, the bonuses have to come from a plan or a pack.

Renewals are manual unless the service says otherwise, and the shipped answer is that they are. GET /public/config → billing.recurring_available is the one field that says which product you are talking to, and everything you build about renewal must read it rather than assume:

Automatic renewal

Only where billing.recurring_available is true.

Send auto_renew: true together with accept_recurring: true on POST /billing/subscribe and the sale becomes a subscription: the payment provider keeps the card — we never see it and never store it — and charges again when the term ends, so the customer never has to come back. Omit both and you get the ordinary fixed term; they default to false and always will.

accept_recurring is a THIRD consent tick, and it is required. The two universal ones (accept_documents, accept_immediate) do not cover it: Cal. B&P §17602(a)(1) wants the consumer's affirmative consent to the automatic renewal terms specifically, and consent to the transaction as a whole is not that. Send auto_renew: true without it and the order is refused with 400 naming the field, and nothing is opened — never quietly downgraded to a fixed term. It is ignored entirely when auto_renew is false.

If you are building your own checkout, the box you show for it has to state, on the same screen and before the tick: what will be charged, how often, when the first renewal falls, and that it can be cancelled at any time from the account. Ours is unticked, always.

# a renewing subscription: three ticks, one order
curl -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
  -d '{"plan_id":2,"months":1,
       "accept_documents":true,"accept_immediate":true,
       "auto_renew":true,"accept_recurring":true,"consent_locale":"en"}' \
  https://mp.dvocorp.com/api/v1/billing/subscribe

It is refused with 409 {"code": "recurring_unavailable"}, never silently downgraded to a fixed term, when the service does not sell subscriptions, when the chosen rail cannot hold a mandate (any crypto rail), when the order is a bundle (a pack cannot be bought over and over for ever), and when a paid term is already running (buying the same plan then extends that term).

The charge repeats once per term; the credits still arrive monthly. This is the one thing to get right. A 12-month subscription is billed once a year and delivers twelve monthly allowances inside that year, each lapsing when the next lands — exactly like a 12-month term bought outright. A renewal buys another twelve. GET /billing/subscription carries both clocks and they are usually far apart:

Field Means
renews_at when the next charge falls due (the provider's own schedule), null when none is expected
next_grant_at when the next monthly allowance lands
renewal_months the charge cadence in months — 12 is one charge a year
renewal_amount_cents / renewal_currency what will be charged, frozen at the moment you subscribed
renewal_status off · active · past_due · paused · cancelling · ended
cancel_at_period_end a cancellation is scheduled; the paid period is still yours
grace_until a renewal charge failed — when access stops if it never succeeds
can_cancel whether the cancel endpoint will answer right now

Your price does not change under you. renewal_amount_cents is fixed when the subscription is created and a later change to the price list reaches new customers only.

# stop the next charge. The period you have paid for is untouched.
curl -X POST -H "Authorization: Bearer $JWT" \
  https://mp.dvocorp.com/api/v1/billing/subscription/cancel
# -> {"auto_renew":false,"renewal_status":"cancelling",
#     "cancel_at_period_end":true,"renews_at":null,
#     "period_end":"2027-02-25T10:00:00Z","months_total":6,"can_cancel":false}

Cancelling is never a refund and never a clawback. period_end does not move, every month you paid for is still delivered on its own boundary, and the term closes on the day it was always going to close. Only the mandate ends. 409 {"code": "not_recurring"} means there was nothing renewing to stop.

The customer can always do it themselves, from the browser. The control lives on their own account page at /profile#subscription — the same medium they subscribed in, behind no menu and no support form, one click from the balance chip in the header of every page. Read can_cancel before you render a control of your own: it asks the same three questions the endpoint raises on, so a button offered on it will never answer 409.

What we email a subscriber

Three letters, sent automatically. Each one states the amount, the date, and carries a link straight to the cancel control — you do not have to build any of them, and you should not send your own copies.

When What it says
Before a renewal charge what will be taken, on what date, at what cadence, and how to stop it first
A renewal charge failed that nothing was taken and nothing granted, how long the account keeps working (grace_until), and that the card is fixed with the payment provider
A cancellation an acknowledgement, and the date access actually ends (period_end, which does not move)

How much warning before a charge. Two operator settings, picked by the charge cadence, not by the grant cadence:

Cycle Setting Default
12 months or more subscriptions_renewal_notice_days_annual 30 days
shorter than a year subscriptions_renewal_notice_days_short 7 days

The annual default sits in the middle of the 15–45 day window Cal. B&P §17602(b) and NY GBL §527-a(d) both require for terms of a year or more. The short one is a courtesy no statute requires — a charge nobody remembers agreeing to becomes a chargeback either way — and both are clamped to the cycle itself, so a window longer than the term can never make the notice arrive as a receipt for the charge that just happened.

A mail failure is never a billing failure: the letters are queued on the subscription row and retried, and nothing about a charge, a grant or a cancellation waits on one.

If a renewal charge fails — an expired card, a decline — the payment provider retries on its own schedule and renewal_status becomes past_due. Access continues until grace_until; no new allowance is granted in the meantime, because no money arrived to pay for one, so the tier, the priority and the balance you are still holding stay and nothing new is added. A charge that lands clears the grace and delivers the cycle at once. If none ever does, the term closes on the ordinary schedule and the last allowance is swept.

Refunds stop future months and never take back delivered ones. If a multi-month order is refunded, the months not yet handed out are withdrawn and the term ends at the close of the month you are in. Allowances already granted stay in your balance — nothing is clawed back.

Your price does not go up. The price and allowance you first pay for are locked to your account for that plan. If the list price later rises, you keep yours (is_locked_price: true, with list_price_cents showing what new customers pay). If it falls below yours, you get the lower one.

Paying. redirect_url is a hosted checkout page — open it in a browser and finish the payment there. Cards go through LiqPay, crypto through Coinbase Commerce, and both can be open at the same time: GET /billing/providers is the list you may pick from, provider on the checkout call is your pick, and the response echoes the rail the order was opened on. Omit provider and you get the default (is_default). A rail that is closed is refused (400), never silently swapped for another one — nobody's card gets charged because the crypto rail went down. Nothing else in the flow changes with the rail, and an order already opened still settles even if that rail is switched off while you are paying.

GET /billing/providers returns [] for the same reasons the price lists do: no rail is open, or payments are not enabled for your account.

Prices are quoted in USD and may be charged in another currency. A card is charged in the merchant account's settlement currency (UAH for the Ukrainian merchant) at the rate shown on the checkout page, so the figure there can differ from price_cents. Your order, your ledger and your receipts stay in the currency the price was quoted in.

Payment states. status follows waiting → confirming → paid. Bonuses land only on paid, which is driven by the provider's callback — expect it a moment after the redirect returns, not instantly. A declined card leaves the order waiting, not failed, precisely so you can retry the same order with another card. An underpaid invoice becomes partially_paid and is not credited automatically (a card cannot underpay; a crypto transfer can); contact support with the order_id. expired and failed grant nothing and charge nothing.

You never lose a payment to a lost callback. If the provider's callback never reaches us, open payments are polled independently and settled from the provider's own record, so a paid order is credited either way — occasionally a couple of minutes later than usual. Poll GET /billing/balance rather than assuming the redirect meant delivery. If an order is still waiting 48 hours after payment, that is the point to contact support with the order_id.

Plans and packs are hidden until payments are live. GET /billing/plans and GET /billing/packages return [] when no payment provider is configured, so you will never be quoted a price you cannot actually pay. Payments are also gated per account: on an account that isn't cleared for billing yet, starting a purchase (POST /billing/subscribe or POST /billing/checkout) is refused with 403 {"detail":"payments are not enabled for this account yet"}. Reading your balance (GET /billing/balance, or GET /me/ping with a key) always works, and so does spending it.

Packs can additionally require a plan (billing.packs_require_subscription on GET /public/config). That is a rule about opening an order, not about the balance: bonuses already bought never expire and stay spendable whatever happens to a subscription.

Getting money back

Three endpoints, and like the rest of this section they take a panel session Bearer token, not an API key — withdrawing from a contract is an act of the person who concluded it.

Endpoint What it does
GET /api/v1/billing/payments/{order_id}/refund-quote what this order would return right now, and why it would not
POST /api/v1/billing/payments/{order_id}/refund withdraw from the contract, or ask for a refund under the policy
GET /api/v1/billing/refunds your own requests and where each one stands

Take the quote first. It is a read — it commits nobody to anything — and it is the only place the arithmetic is visible:

curl -s -H "Authorization: Bearer $TOKEN" \
  https://mp.dvocorp.com/api/v1/billing/payments/ord_XXXX/refund-quote
{
  "order_id": "ord_XXXX",
  "currency": "USD",
  "amount_cents": 1200,
  "paid_cents": 2000,
  "already_refunded_cents": 0,
  "credits_sold": 1000,
  "credits_returnable": 600,
  "credits_spent": 400,
  "reason": "withdrawal",
  "deadline": "2026-09-19T10:04:11Z",
  "withdrawal_deadline": "2026-09-19T10:04:11Z",
  "blocked": "",
  "eligible": true,
  "payout_manual": false,
  "open_refund_id": null
}

Then confirm:

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"note":"bought the wrong pack"}' \
  https://mp.dvocorp.com/api/v1/billing/payments/ord_XXXX/refund

What comes back is what you did not spend. Bonuses you already spent paid for jobs that completed and the processing behind them has already run, so they are not refundable — and they are never clawed back off your balance either. The unspent part is returned in full, at the price you paid for it, and the money is that same fraction of the charge. credits_spent on the quote is that figure, printed rather than implied.

Bonuses are spent soonest-expiring first, so a purchased pack (which never expires) is the last thing touched. Burning a free daily allowance does not reduce what a pack is worth back.

A plan term also returns the months it has not delivered. Those were never granted, so nothing comes off the balance for them; the money for them goes back and the term stops accruing.

reason says which right you are exercising. "withdrawal" is the statutory right to withdraw from a distance contract — 14 days — and is what the control in the web app is labelled "withdraw from contract here" for. "policy" is our own Refund Policy, which promises unused bonuses back for 30 days and is the broader of the two. Both are admin-set; read the dates off the quote rather than counting days yourself.

blocked is machine-readable and amount_cents is then 0. not_settled (the order never completed), window_closed, nothing_left (every bonus from this order was spent — the request is answered, and the answer is nothing), already_open (a request is with us), fully_refunded.

payout_manual: true means a person sends the money. Card orders are refunded over the provider's API. A crypto order cannot be: a blockchain can only be debited by the wallet's owner, so the refund is approved, the unspent bonuses come off the balance, and the transfer is made by hand. You get an email when it goes out.

You get the confirmation in writing. Asking sends an acknowledgement to your account email, and the outcome is emailed too — that is your record of the request, not the page you asked from.

A refund never returns more than the order charged, and a retried request never pays twice.