Quota
/ docs
Dashboard
Docs/Recipes/Top up an OAuth user wallet

Top up an OAuth user wallet

Move credits from your developer wallet into a Quota user's wallet — atomically, idempotently, and observably. Common use cases: promo grants for new signups, refund credits after a support ticket, loyalty bonuses, comp credits when a model misbehaves.

Is this the right recipe?

Yes if: the user already exists in Quota (they signed in via Sign in with Quota), and you want to give them more credits from your developer wallet. No if: you want the user to pay you for credits (use POST /api/payments/checkout instead) or the user should bring their own pre-funded Quota wallet (use Connect Quota Wallet).

01 Confirm the user's Quota UUID

Funding targets the user by their Quota UUID — the sub claim from /oauth/userinfo, not an external_user_id you assigned. If your app stores the user via NextAuth / Clerk / Auth0, that's the field labeled quota_sub / quotaProfile.id / similar in the user's session.

// From a Next.js Route Handler or Server Component:
import { getQuotaUser } from "@usequota/nextjs/server";

const user = await getQuotaUser();
if (!user) throw new Error("not signed in");

console.log(user.id);
// → "e4f8b9a1-3c2d-4e5f-9a0b-1c2d3e4f5a6b"
// Store this as quota_user_id on your own user row.

02 Call the funding endpoint

POST /v1/funding/oauth_user with your developer API key. Quota debits your wallet for the same amount it credits the user's wallet, in a single DB transaction — there is no intermediate state where the money exists in neither wallet.

curl https://api.usequota.ai/v1/funding/oauth_user \
  -H "Authorization: Bearer $QUOTA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: promo_2026_05_e4f8b9a1" \
  -d '{
    "user_id": "e4f8b9a1-3c2d-4e5f-9a0b-1c2d3e4f5a6b",
    "amount": 500000,
    "description": "Welcome bonus — May 2026"
  }'
Idempotency keys are shared with /funding/credit

The (app_id, idempotency_key) uniqueness index is shared between /v1/funding/credit (external-user-id flow) and /v1/funding/oauth_user. Reusing a key across the two endpoints returns 409 conflict — by design, to prevent "same logical grant credited twice via two different code paths". Give cross-endpoint grants distinct keys (e.g. prefix with oauth_ vs extid_).

03 Verify and react

The response contains the user's new balance_after and the ledger_id of the transfer. Persist theledger_id on your side — that's your audit trail and your reconcile key against the user's in-Quota ledger view.

{
  "success": true,
  "user_id": "e4f8b9a1-3c2d-4e5f-9a0b-1c2d3e4f5a6b",
  "amount": 500000,
  "balance_after": 1500000,
  "ledger_id": "f0a92e74-2c8b-4ad5-9c1e-94b1f3c8a275"
}

04 (Optional) Subscribe to balance.updated

If your app displays the user's balance live, the funding call fires a balance.updated webhook to the OAuth client that owns the user. Wire your handler at the URL you set when registering the app — Quota signs the payload so you can verify it didn't come from somewhere else.

{
  "event": "balance.updated",
  "user_id": "e4f8b9a1-3c2d-4e5f-9a0b-1c2d3e4f5a6b",
  "new_balance": 1500000,
  "amount_spent": -500000,
  "model": null,
  "endpoint": "/v1/funding/oauth_user"
}

Note the negative amount_spent — funding events reuse the same webhook event shape as usage events, with a negative value to mean "credits added". The same path powers refunds.

05 When things go wrong

The funding endpoint returns the standard error envelope. Common cases:

  • 402 insufficient_credits — your developer wallet is below the transfer amount. Top up your own wallet (Stripe Checkout via POST /api/payments/checkout) and retry.
  • 404 user_not_found — no Quota user has that user_id. Common cause: storing anexternal_user_id in the field that expects a Quota UUID — they look similar enough to confuse.
  • 409 conflict — idempotency key reused (often across /funding/credit and /funding/oauth_user). The response includes the existing ledger_id — treat it as a successful duplicate, not an error.
  • 400 invalid_request — usually a malformed UUID or a non-positive amount.