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 |
- Pausing a key is reversible — the same plaintext works again after you re-enable it. Deleting is permanent; pause instead if you might need it.
- A key revoked by an administrator can't be re-enabled or deleted by you.
- You cannot mint a key with a key — key management needs a panel session.
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 credits, 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 credits.
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 credits the signup bonus 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 credits 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).
- These are browser flows — the SPA drives them. Nothing here accepts (or needs) an API key, and API keys are unaffected by how you signed in.
current_passwordis required only if you already have one. An account created through Google sets its first password without it — which is how you keep access if Google is ever unavailable or you move to another Google account. An administrator can also issue a password from the panel.- Detaching Google is refused while it is your only way in.
- The button appears only when the deployment is configured for it
(
GET https://mp.dvocorp.com/api/v1/public/config→google_auth_enabled).
Credit 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.
Plans & credits
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.
# 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":1900,"currency":"USD",
# "period_credits":6000,"period_days":30,"priority":5,
# "is_locked_price":false,"list_price_cents":1900}]
# 2. start a plan purchase -> open `redirect_url` in a browser to pay
curl -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{"plan_id":2}' https://mp.dvocorp.com/api/v1/billing/subscribe
# -> {"order_id":"ord_...","redirect_url":"https://nowpayments.io/payment/...",
# "status":"waiting","balance":100}
# 3. or buy a one-off credit pack (these never expire)
curl -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{"package_id":1}' https://mp.dvocorp.com/api/v1/billing/checkout
# 4. 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"}
GET https://mp.dvocorp.com/api/v1/billing/subscription returns the active term
(plan_name, period_end, credits_per_period, auto_renew) or null.
How credits behave. You have one balance. Plan credits are granted each
paid period and are void at its end — an unused remainder is replaced by the
new allowance, not added to it. Credits you buy as a pack, and any an
administrator grants you, never expire. A charge always consumes the
soonest-expiring credit first, so nothing you paid extra for is lost to a reset.
expiring in the balance response is the part that lapses on expires_at; it
is not a separate wallet.
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. Every confirmed account
is topped up with a small number of credits once a day, usable on anything the
API offers. Like plan credits 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). 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 credits have to come from a plan or a pack.
Renewals are manual. Crypto payments cannot be pulled from a wallet on a
schedule, so auto_renew is always false and nothing is ever charged without
you starting it. Buy the next term from /pricing before the current one ends.
POST /billing/subscribe on the plan you already hold simply starts a new term.
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.
Payment states. status follows waiting → confirming → paid. Credits
land only on paid, which is driven by the provider's callback — expect a few
minutes for network confirmations after the redirect returns. An underpaid
invoice becomes partially_paid and is not credited automatically; 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.