Learn how to grant or top up user balances programmatically using the Deposit API. This is useful for signup bonuses, subscription renewals, refunds/goodwill, migrations, or promotions.
This guide applies to Default mode (you control payments). In Shared mode, users fund a hosted, cross‑app wallet and you don’t deposit credits directly. See Shared Mode.
1

Prerequisites

  • Default mode enabled and your Paywalls API key configured.
  • A stable, pseudonymous user id selected. See User Identity.
2

Call the Deposit API

Use POST /v1/user/balance/deposit/post. Amount is a string in your configured currency (e.g., “10”).
curl -X POST https://api.paywalls.ai/v1/user/balance/deposit \
-H "Authorization: Bearer $PAYWALLS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $UNIQUE_EVENT_ID" \
-d '{
	"user": "user_123",
	"amount": "10",
	"metadata": {
		"reason": "signup_bonus",
		"source": "marketing_campaign_2025_09"
	}
}'
3

Verify and display balances

  • The deposit appears in the Ledger immediately.
  • Optionally fetch the updated balance for display using GET /user/balance.
Include Idempotency-Key so repeated webhooks or retries do not create duplicate credits.

Common scenarios

1. Signup bonus on registration

Grant a one‑time credit when a new account is created.
  • Amount: small trial amount (e.g., “1.00”)
  • Metadata: {"reason":"signup_bonus","signup_event":"evt_signup_123"}
// Example: in your user-created handler
await fetch("https://api.paywalls.ai/v1/user/balance/deposit", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PAYWALLS_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `signup:${userId}`, // stable for this logical event
  },
  body: JSON.stringify({
    user: userId,
    amount: "1.00",
    metadata: { reason: "signup_bonus" },
  }),
});

2. Subscription renewal credits

Credit users monthly after your PSP confirms payment (e.g., Stripe invoice.payment_succeeded).
  • Amount: your plan’s included usage value (e.g., “10.00”)
  • Metadata: include invoice id, plan, period_start/end for reconciliation
// Example: in your Stripe webhook handler (invoice.payment_succeeded)
const {
  id: invoiceId,
  payment_intent: pi,
  customer,
  lines,
} = event.data.object;
await fetch("https://api.paywalls.ai/v1/user/balance/deposit", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PAYWALLS_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": invoiceId, // prevents duplicates on retries
  },
  body: JSON.stringify({
    user: mapStripeCustomerToUserId(customer),
    amount: planIncludedCreditsAmount(lines), // e.g., "10.00"
    metadata: {
      reason: "subscription_included_credits",
      invoice_id: invoiceId,
      payment_intent: pi,
    },
  }),
});
See Connect Stripe for Default mode setup.

No‑code (Zapier, n8n)

You can call the Deposit API from no‑code tools to credit balances without custom code.
Prefer injecting a stable user from your CRM/DB. For no‑code patterns, see No-code: Zapier, n8n, and other flows.

Best practices

  • Idempotency first: reuse the same Idempotency-Key on retries/webhook replays.
  • Pseudonymous identity: avoid PII in user ids. See User Identity.
  • Don’t expose keys: keep PAYWALLS_API_KEY server/edge only.
  • Reconciliation: store invoice/payment ids in metadata. Use the Ledger and Analytics & Reporting.
  • Testing: use Stripe test mode in Default mode staging. See Test keys & Environments.