> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.morphiaone.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.morphiaone.ai/_mcp/server.

# Credits and usage

## Running out is a 402, not a 400

```json
{
  "error": {
    "code": "INSUFFICIENT_CREDITS",
    "message": "This request needs 1 credit and the account has 0.",
    "request_id": "c7a4e2f0-5b6d-4a18-9e3c-2f1d0b8a7c64",
    "details": []
  }
}
```

`402 INSUFFICIENT_CREDITS` on a submission. Nothing was queued, nothing was spent,
and retrying the identical call will get the identical answer until the balance
changes — so this is not a retryable failure, it is an operational one. Route it to
whoever can top up the account, not to your backoff loop.

A submission that is accepted (`202`) has committed its cost. A submission that is
refused has not. The failure modes in between are worth being precise about:

| Outcome                                               | Charged?                                           |
| ----------------------------------------------------- | -------------------------------------------------- |
| `202` accepted, job later `succeeded`                 | yes                                                |
| `202` accepted, job later `failed`                    | no — a failed job's hold is released, not captured |
| `200` idempotent replay of an earlier submission      | no — you are charged once                          |
| `402`, `429`, `503`, or any refusal before acceptance | no                                                 |
| Your client timed out waiting, job finished anyway    | yes — see [Asynchronous jobs](/concepts/jobs)      |

The last row is why an `Idempotency-Key` on every submission is not optional advice.
Without one, a timeout followed by a resubmission is two charges for one image.

## When a submission is refused for credits

Credits are tracked per account. Whether a submission without enough units is refused
depends on your account:

* **Enforced.** A submission that needs more units than the account can spend is
  refused `402 INSUFFICIENT_CREDITS` before anything is queued.
* **Not enforced.** Submissions are accepted whatever the balance, and no units are
  held or charged for them. You will not receive a `402`; what you are invoiced is
  governed by your commercial agreement.

Read `enforced` from the balance below rather than assuming either. A `pk_test_` key
never spends credits, in either case — see [Test mode](/getting-started/test-mode).

## Reading the balance

`GET /v1/credits/balance` returns the balance of the account that owns the key, in
abstract units, never money. It is enabled per account; where it is not, it answers
`404` as an unknown path does.

| Field               | Meaning                                                                      |
| ------------------- | ---------------------------------------------------------------------------- |
| `available_units`   | Settled units minus units held by accepted, unsettled jobs. May be negative. |
| `held_units`        | Units held by accepted jobs that have not settled yet.                       |
| `settled_units`     | Grants and adjustments minus captured charges.                               |
| `test_funded_units` | Units funded by test payments. Not spendable while `enforced` is true.       |
| `spendable_units`   | What a new submission may spend.                                             |
| `enforced`          | `true` when a submission without the units is refused `402`.                 |
| `low_balance`       | `true` when the balance is low enough to warrant a top-up.                   |
| `unit_costs`        | What one accepted job of each operation costs, keyed by operation.           |

**`TypeScript`**

```ts title="TypeScript"
const balance = await client.credits.balance();
if (balance.enforced && balance.low_balance) alertBilling(balance.spendable_units);
```

**`Python`**

```python title="Python"
balance = client.credits.balance()
if balance["enforced"] and balance["low_balance"]:
    alert_billing(balance["spendable_units"])
```

**`REST`**

```bash title="REST"
curl -sS -H "X-API-Key: ${API_KEY}" "${BASE_URL}/v1/credits/balance"
```

Polling the balance is a good way to warn before you run out. It does not replace
handling `402`: another request can spend the last unit between your read and your
submission. A balance that cannot be read answers `503`, never a zero balance.

## What `GET /v1/usage` gives you

Job counts for your own account, per UTC day and per operation, over a half-open
`start_date` / `end_date` window: `start_date` is included and `end_date` is not. With
neither, the window is the last thirty full days plus today. A window longer than 92
days, or one that ends before it starts, is `400 INVALID_USAGE_RANGE`.

```bash
curl -H "X-API-Key: ${API_KEY}" \
  "${BASE_URL}/v1/usage?start_date=2026-09-01&end_date=2026-09-25"
```

```json
{
  "period": { "start_date": "2026-09-01", "end_date": "2026-09-25" },
  "as_of": "2026-09-24T10:00:00Z",
  "totals": {
    "jobs_accepted": 15, "jobs_succeeded": 11, "jobs_failed": 2,
    "jobs_cancelled": 1, "jobs_pending": 1, "billable_jobs": 11
  },
  "products": [
    { "operation": "makeup.apply", "jobs_accepted": 6, "jobs_succeeded": 4,
      "jobs_failed": 1, "jobs_cancelled": 0, "jobs_pending": 1, "billable_jobs": 4 },
    { "operation": "skin.analyze", "jobs_accepted": 9, "jobs_succeeded": 7,
      "jobs_failed": 1, "jobs_cancelled": 1, "jobs_pending": 0, "billable_jobs": 7 }
  ],
  "days": [
    { "date": "2026-09-21", "operation": "makeup.apply", "jobs_accepted": 4,
      "jobs_succeeded": 3, "jobs_failed": 1, "jobs_cancelled": 0,
      "jobs_pending": 0, "billable_jobs": 3, "final": true }
  ]
}
```

A row exists only for a day and operation with activity, so iterate `days`; do not
index it by day offset. A window with no activity answers zeros in `totals` and empty
`products` and `days`, not a `404`.

> **Warning**
>
> These are **operational aggregates, not billing authority.** They are the right
> input for a dashboard, a capacity conversation, or noticing that yesterday's job
> count tripled. They are the wrong input for reconciling an invoice — that comes
> from your commercial agreement, and the two can legitimately differ.

Field meanings:

* `jobs_accepted` — submissions accepted, successful or not.
* `jobs_succeeded`, `jobs_failed`, `jobs_cancelled`, `jobs_pending` — where those
  jobs stand as of `as_of`. A pending job moves into one of the other three later, so
  a count can change. A cancelled job was cancelled by you before processing started
  and is never billed.
* `billable_jobs` — jobs whose terminal outcome the credit policy captures.
* `final` — `false` for a day within the last 48 hours, whose counts may still move.
  Treat only a `final` day as settled.

The endpoint is authenticated and reports the environment of the key you call it
with: a `pk_test_` key reads test usage, a `pk_live_` key live usage. The report names
no account, and there is no way to read another account's usage.

## Pricing

Not documented here, and deliberately. Rates, included volumes and overage terms
are in your commercial agreement, and a number on a docs page would eventually
contradict a contract. Ask your account contact.