---
title: "Add credits to a tenant (admin only)"
method: POST
path: "/v1/billing/credits"
---

# Add credits to a tenant (admin only)

`POST /v1/billing/credits`

## Request

- Base URL: `https://api.clichefactory.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.clichefactory.com/v1/billing/credits`
- Auth: none declared

Issue credits to a tenant and record the sale.

This is the currency boundary of the whole system: everything downstream of
it is credits, which have no currency.

**Validate, don't derive.** The caller sends the credit count the customer
was shown at checkout, and this endpoint confirms it against the active
price list. It does not compute a second count of its own — that is how a
customer ends up shown one number and granted another when a price publish
lands mid-checkout, or when a cached pricing scalar goes stale.

Idempotent on ``provider_txn_id``: a webhook retry returns the original
grant rather than crediting the tenant twice. That is not a nicety — the
Django webhook deletes its own idempotency row when a call to this endpoint
times out, so a grant that committed but whose response was lost gets
retried, and only this side can tell the difference.

## Headers

- `X-API-KEY` string, nullable

## Request body

- AddCreditsRequest: Credit a tenant, in any supported currency. Two accepted shapes. The modern one names the credits and the money explicitly:: {"tenant_id": "...", "credits": 9500, "currency": "EUR", "gross_amount_minor": 1900, "tax_amount_minor": 342, "provider": "paddle", "provider_txn_id": "txn_01h..."} The legacy one passes ``amount_usd`` and lets the server derive credits at the USD issue price. It is deprecated but still supported so the Django app does not have to ship in lockstep with this change. ``provider_txn_id`` is the webhook idempotency key: a Paddle retry with the same id returns the original grant instead of crediting twice.
  - `tenant_id` string, required
  - `credits` integer, nullable: Credits to issue. Derived from the amount when omitted.
  - `currency` string: ISO-4217 code. EUR or USD.
  - `gross_amount_minor` integer, nullable: What the customer paid, incl. tax, in minor units.
  - `tax_amount_minor` integer
  - `net_amount_minor` integer, nullable: Ex-tax, ex-provider-fee. Defaults to gross - tax.
  - `provider` string: 'paddle' | 'invoice' | 'admin' | 'migration'
  - `provider_txn_id` string, nullable: Payment-provider transaction id; the idempotency key. Required for provider='invoice', where it is the invoice number.
  - `provider_net_minor` integer, nullable: The provider's own reported earnings, if known.
  - `provider_settle_ccy` string, nullable
  - `credit_price_list_id` integer, nullable
  - `reason` string
  - `amount_usd` number, nullable

## Response `200`

Successful Response

- AddCreditsResponse
  - `tenant_id` string, required
  - `credits_added` integer
  - `currency` string
  - `gross_amount_minor` integer
  - `new_balance_credits` integer
  - `grant_id` integer, nullable
  - `credited` boolean
  - `amount_usd` number
  - `new_balance_usd` number

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/clichefactory/apis/aio-server.md) · [All operations](https://skmtc.dev/clichefactory/apis/aio-server/llms.txt) · [OpenAPI document](https://skmtc.dev/clichefactory/apis/aio-server/revisions/9bd59f9bbb31?raw)
