Admin Billing
GET /api/admin/billing/catalog
Section titled “GET /api/admin/billing/catalog”Get Billing Catalog
Every plan and pack with its Polar product mapping, plus collisions.
null on a product id is a state, not a gap: it means the row is not
sellable through Polar, which is correct for trial and enterprise
and is what lets the checkout API refuse them without a special-case list.
collisions is what the database cannot enforce. It is advisory — the
webhook resolver is the authority and its answer to an ambiguous product id
is unresolved, i.e. a payment that lands nowhere until somebody fixes
the mapping here.
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | BillingCatalogResponse |
| 422 | Validation Error | HTTPValidationError |
GET /api/admin/billing/events
Section titled “GET /api/admin/billing/events”List Billing Events
The webhook ingress log, newest first, payloads included.
status_counts covers the whole table rather than the page, because the
question this screen answers is “is any money stranded”, and a page of
applied rows says nothing about the unresolved one behind it.
Payloads are returned whole. That is the point of storing them: when a
customer says “I paid and got nothing”, this plus credit_ledger is the
entire answer, and Polar’s own delivery log is not queryable from our side.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | no | |
| offset | query | integer | no | |
| org_id | query | string (uuid) | no | |
| status | query | string | no | Filter to one BillingEventStatus value. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | BillingEventListResponse |
| 422 | Validation Error | HTTPValidationError |
POST /api/admin/billing/events/{event_id}/replay
Section titled “POST /api/admin/billing/events/{event_id}/replay”Replay Billing Event
Re-run a stored delivery’s effect.
Re-runs the effect, not the status stamp. Marking a row applied
without moving any money would turn a stranded payment into an invisible one,
which is strictly worse than leaving it stranded. Re-applying is safe because
every writer underneath is idempotent on its own deterministic ledger key.
Refuses an applied row: that status is the only thing a redelivery is
allowed to short-circuit on, and re-running it would be the one case the
ledger keys are not protecting against a second human decision.
The effect itself lives in the webhook ingress. When that is not in the
build this answers 503 naming the symbol it looked for, and touches nothing —
no status change, no attempts bump — so the row stays exactly as
diagnosable as it was.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| event_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | BillingEventReplayResponse |
| 422 | Validation Error | HTTPValidationError |
GET /api/admin/credit-packs
Section titled “GET /api/admin/credit-packs”List Credit Packs
Every pack, active or retired, in display order.
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | PackProductRow[] |
| 422 | Validation Error | HTTPValidationError |
POST /api/admin/credit-packs
Section titled “POST /api/admin/credit-packs”Create Credit Pack
Create a credit pack. code must be unique.
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| code | string | yes | |
| credits | number | string | yes | Credits granted on payment. Must be positive and representable in NUMERIC(20, 6) — a pack worth zero credits is a mistake, not a state anyone chose. |
| is_active | boolean | no | |
| name | string | yes | |
| polar_product_id | string (uuid) | no | |
| price_currency | string | no | ISO 4217 alpha-3 the price is quoted in. Set it to whatever the Polar product is priced in — the figure above carries no currency of its own, and every surface that renders it reads this. |
| price_display | number | string | no | Display only. The price actually charged is the one on the Polar product; nothing bills from this column. |
| sort_order | integer | no |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | Successful Response | PackProductRow |
| 422 | Validation Error | HTTPValidationError |
PUT /api/admin/credit-packs/{pack_id}
Section titled “PUT /api/admin/credit-packs/{pack_id}”Update Credit Pack
Replace a pack. Complete payload, like a plan write.
Editing credits does not retro-adjust anyone: a purchase already made
wrote its own ledger row for the amount that was in force at the time, and
the ledger is append-only. This changes what the next buyer gets.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| pack_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| code | string | yes | |
| credits | number | string | yes | Credits granted on payment. Must be positive and representable in NUMERIC(20, 6) — a pack worth zero credits is a mistake, not a state anyone chose. |
| is_active | boolean | no | |
| name | string | yes | |
| polar_product_id | string (uuid) | no | |
| price_currency | string | no | ISO 4217 alpha-3 the price is quoted in. Set it to whatever the Polar product is priced in — the figure above carries no currency of its own, and every surface that renders it reads this. |
| price_display | number | string | no | Display only. The price actually charged is the one on the Polar product; nothing bills from this column. |
| sort_order | integer | no |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | PackProductRow |
| 422 | Validation Error | HTTPValidationError |
DELETE /api/admin/credit-packs/{pack_id}
Section titled “DELETE /api/admin/credit-packs/{pack_id}”Delete Credit Pack
Delete a pack.
Deactivating (is_active=false) is nearly always the right move instead:
a retired pack that stays in the table keeps an old purchase explainable,
and a deleted one leaves a paid order.paid replay with no product to
resolve against. Deletion stays available for a pack created by mistake and
never sold.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| pack_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 204 | Successful Response | |
| 422 | Validation Error | HTTPValidationError |
GET /api/admin/orgs/{org_id}/billing
Section titled “GET /api/admin/orgs/{org_id}/billing”Get Org Billing
This org’s external subscription, Polar’s verbatim status, and its link.
Two status columns, on purpose. external_status is what Polar said;
status is what our period job and credit gates read. Conflating them
would let a Polar state we have never heard of silently mean “active” or
silently mean “blocked”, and both are wrong — so they are shown side by side
and a disagreement is something a human can see.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| org_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | OrgBillingResponse |
| 422 | Validation Error | HTTPValidationError |
PUT /api/admin/plans/{plan_id}/polar-products
Section titled “PUT /api/admin/plans/{plan_id}/polar-products”Set Plan Polar Products
Register (or clear) a plan’s Polar product ids.
Sending null is a deliberate write meaning “not sellable”, not an
omission — which is why this is its own endpoint rather than two more fields
on the plan editor’s total-replace payload.
A duplicate id comes back as 409 rather than a 500: the partial unique index is the guard that stops two plans resolving one product ambiguously, and an operator hitting it deserves to be told which guard they hit.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| plan_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| polar_product_id | string (uuid) | no | |
| polar_product_id_annual | string (uuid) | no |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | BillingCatalogResponse |
| 422 | Validation Error | HTTPValidationError |