> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fribl.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Tokens & Billing

> How usage-based tokens work, how to check your balance and usage, and how each endpoint is priced

# Tokens & Billing

The Fribl API is metered with **tokens**. Every billable operation — analyzing a CV, parsing a job, running a match, or sourcing candidates — deducts tokens from your workspace balance. Read-only operations (listing, retrieving, polling status, and the Skills endpoints) are free.

<CardGroup cols={3}>
  <Card title="Check balance" icon="wallet" href="#check-your-balance">
    `GET /credits/balance` — real-time balance for your workspace.
  </Card>

  <Card title="Review usage" icon="clock-rotate-left" href="#review-your-usage">
    `GET /credits/history` — a paginated ledger of every charge, refund, and grant.
  </Card>

  <Card title="See pricing" icon="tags" href="#live-pricing">
    `GET /credits/costs` — the live price list for every billable action.
  </Card>
</CardGroup>

## How tokens work

* **Balance is per-workspace.** Every API key belongs to a workspace, and all keys in that workspace draw from one shared balance.
* **You're charged per unit of work.** Submitting 5 CVs in one request charges 5 × the CV price. Each unit gets its own ledger entry.
* **You're not charged for failures.** If a task ends in `FAILED`, or the API returns a server error (`5xx`), the tokens for that unit are refunded automatically.
* **Retries are safe.** Charges are idempotent per task, so re-sending a request that already succeeded never double-charges.
* **The ledger is the source of truth.** Every movement — charge, refund, grant, or expiry — is recorded as an immutable entry you can read back via the history endpoint.

<Info>
  Tokens are purchased and managed in the **[Fribl Console](https://console.fribl.co)**, where you also create API keys and administer your organization. The API itself is read-only with respect to your balance — it reports usage and pricing, but tokens are added in the Console.
</Info>

## Pricing

Each billable endpoint maps to one or more **actions**, and each action has a price in tokens. The table below shows the standard pricing. Prices can change, so always treat [`GET /credits/costs`](#live-pricing) as the authoritative source — don't hardcode these values.

| Action                    | Tokens | What it covers                                 |
| ------------------------- | ------ | ---------------------------------------------- |
| `cv_processing`           | 1      | Process a single CV                            |
| `job_processing`          | 1      | Process a single job description               |
| `job_matching`            | 2      | Match candidates against a job                 |
| `sourcing_search_fast`    | 3      | A fast sourcing search                         |
| `sourcing_search_pro`     | 10     | A deeper "pro" sourcing search                 |
| `sourcing_ingest`         | 2      | Import one sourced candidate                   |
| `sourcing_reveal_email`   | 2      | Reveal one candidate's email                   |
| `sourcing_reveal_phone`   | 2      | Reveal one candidate's phone                   |
| `sourcing_insights`       | 1      | Per-candidate AI insights on a search          |
| `sourcing_high_freshness` | 1      | Per-candidate fresh-profile lookup on a search |

### What each endpoint charges

| Endpoint                   | Charged                                                   | Quantity                                      |
| -------------------------- | --------------------------------------------------------- | --------------------------------------------- |
| `POST /cvs/analyze`        | `cv_processing`                                           | One per CV in `inputs`                        |
| `POST /cvs/analyze/files`  | `cv_processing`                                           | One per uploaded file                         |
| `POST /jobs/analyze`       | `job_processing`                                          | One per job in `inputs`                       |
| `POST /jobs/analyze/files` | `job_processing`                                          | One per uploaded file                         |
| `POST /match`              | `job_matching`                                            | One per match request                         |
| `POST /sourcing/search`    | `sourcing_search_fast` / `sourcing_search_pro` + premiums | Base once, premiums per candidate (see below) |
| `POST /sourcing/ingest`    | `sourcing_ingest`                                         | One per candidate in `selected_ids`           |

## Live pricing

Fetch the current price list at any time. This is the canonical pricing source — build your cost estimates against it rather than the static table above.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api-service.fribl.co/api/v1/credits/costs \
    -H "x-api-key: your-api-key-here"
  ```

  ```python Python theme={null}
  import requests

  resp = requests.get(
      "https://api-service.fribl.co/api/v1/credits/costs",
      headers={"x-api-key": "your-api-key-here"},
  )
  for cost in resp.json()["data"]:
      print(cost["action"], cost["cost_in_credits"])
  ```

  ```javascript JavaScript theme={null}
  const resp = await fetch(
    "https://api-service.fribl.co/api/v1/credits/costs",
    { headers: { "x-api-key": "your-api-key-here" } }
  );
  const { data } = await resp.json();
  data.forEach((c) => console.log(c.action, c.cost_in_credits));
  ```
</CodeGroup>

```json Response theme={null}
{
  "data": [
    { "action": "cv_processing",  "cost_in_credits": 1, "description": "Process a single CV" },
    { "action": "job_processing", "cost_in_credits": 1, "description": "Process a job description" },
    { "action": "job_matching",   "cost_in_credits": 2, "description": "Match candidates to a vacancy" },
    { "action": "sourcing_search_fast", "cost_in_credits": 3,  "description": "Fast sourcing search" },
    { "action": "sourcing_search_pro",  "cost_in_credits": 10, "description": "Pro sourcing search" }
  ]
}
```

## Check your balance

Returns the real-time token balance for the workspace the API key belongs to.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api-service.fribl.co/api/v1/credits/balance \
    -H "x-api-key: your-api-key-here"
  ```

  ```python Python theme={null}
  import requests

  resp = requests.get(
      "https://api-service.fribl.co/api/v1/credits/balance",
      headers={"x-api-key": "your-api-key-here"},
  )
  print(resp.json()["data"]["balance"])
  ```

  ```javascript JavaScript theme={null}
  const resp = await fetch(
    "https://api-service.fribl.co/api/v1/credits/balance",
    { headers: { "x-api-key": "your-api-key-here" } }
  );
  const { data } = await resp.json();
  console.log(data.balance);
  ```
</CodeGroup>

```json Response theme={null}
{
  "data": {
    "workspace_id": "ws_a1b2c3d4",
    "balance": 149
  }
}
```

<Tip>
  Call `GET /credits/balance` before a large batch (for example, uploading 100 CVs) to confirm you can cover it. A batch that runs out of tokens partway through is rolled back in full — see [Insufficient tokens](#insufficient-tokens).
</Tip>

## Review your usage

Returns a paginated ledger of every token movement for your workspace, newest first.

| Query parameter | Type               | Default | Notes                                                                                                          |
| --------------- | ------------------ | ------- | -------------------------------------------------------------------------------------------------------------- |
| `limit`         | integer            | `50`    | Maximum entries to return (max `500`).                                                                         |
| `before`        | ISO 8601 timestamp | —       | Return only entries created before this time. Use the `created_at` of the last entry you saw to page backward. |

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api-service.fribl.co/api/v1/credits/history?limit=20" \
    -H "x-api-key: your-api-key-here"
  ```

  ```python Python theme={null}
  import requests

  resp = requests.get(
      "https://api-service.fribl.co/api/v1/credits/history",
      headers={"x-api-key": "your-api-key-here"},
      params={"limit": 20},
  )
  for entry in resp.json()["data"]:
      print(entry["created_at"], entry["action"], entry["amount"])
  ```
</CodeGroup>

```json Response theme={null}
{
  "data": [
    {
      "id": "10473",
      "workspace_id": "ws_a1b2c3d4",
      "action": "job_matching",
      "amount": -2,
      "balance_after": "149",
      "reference_id": "match_9f8e7d6c",
      "created_at": "2026-06-03T10:18:02.114Z"
    },
    {
      "id": "10472",
      "workspace_id": "ws_a1b2c3d4",
      "action": "cv_processing",
      "amount": -1,
      "balance_after": "151",
      "reference_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "created_at": "2026-06-03T10:15:30.000Z"
    }
  ]
}
```

### Reading a ledger entry

| Field           | Meaning                                                                                                                 |
| --------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `action`        | The billable action, or a balance event such as `grant` or `token_expired`.                                             |
| `amount`        | Signed token change: **negative for a charge**, **positive for a refund or grant**.                                     |
| `balance_after` | Your balance immediately after this entry was applied.                                                                  |
| `reference_id`  | The task, match, or session this entry is tied to — useful for reconciling a charge against the request that caused it. |
| `created_at`    | When the entry was recorded (ISO 8601, UTC).                                                                            |

The `action` values you may see in your history:

| `action`                                                        | `amount` | Meaning                                                                                                         |
| --------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
| `cv_processing`, `job_processing`, `job_matching`, `sourcing_*` | negative | A charge for that operation.                                                                                    |
| any of the above                                                | positive | A **refund** of a previous charge (for example, a failed task). The `reference_id` matches the original charge. |
| `grant`                                                         | positive | Tokens added to your workspace (a purchase or a trial grant).                                                   |
| `token_expired`                                                 | negative | Trial or promotional tokens that reached their expiry date.                                                     |

## Sourcing pricing in detail

Sourcing is the only endpoint with variable, per-candidate pricing. `POST /sourcing/search` charges:

1. **A base fee, once** — `sourcing_search_fast` (default) or `sourcing_search_pro` when you set `options.search_type: "pro"`.
2. **Per-candidate premiums** for any options you enable, charged once per candidate actually returned:
   * `insights` → `sourcing_insights`
   * `high_freshness` → `sourcing_high_freshness`
3. **Per-reveal fees** for contact data, charged once per candidate whose data is actually returned:
   * `reveal_emails` → `sourcing_reveal_email`
   * `reveal_phones` → `sourcing_reveal_phone`

<Note>
  Before running the search, the API checks that your balance can cover the **worst case** (every candidate matching every enabled premium). If it can't, the request is rejected up front with a `402` and **nothing is charged** — you never get a half-charged search.
</Note>

<Tip>
  **Pagination is free.** Each search returns a `session_id`. Re-requesting a window that's already cached for that session costs **0 tokens** (the response includes `"cached": true`). You're only charged again when the search reaches new candidates.
</Tip>

`POST /sourcing/ingest` charges `sourcing_ingest` once per candidate in `selected_ids`. Each candidate is charged independently, so a single failed import is refunded on its own without affecting the others.

## Refunds and idempotency

You only pay for work that succeeds.

* **Failed tasks are refunded automatically.** When a CV, job, or sourcing task ends in `FAILED`, its charge is reversed. The refund appears in your history as a positive `amount` on the original action.
* **Server errors are refunded automatically.** If the API returns a `5xx`, any tokens charged for that request are reversed.
* **Partial batches roll back.** If you submit a batch and the balance runs out partway through, everything already charged in that request is refunded and the whole request returns `402` — you're never left having paid for half a batch.
* **Retries don't double-charge.** Each unit of work is charged against a stable key, so re-sending an identical request returns the original result without charging again.

<Note>
  Client errors (`4xx`) are **not** refunded, because no work was attempted — a `400` for a malformed body, for example, is rejected before anything is charged.
</Note>

## Insufficient tokens

When your balance can't cover a request, the API responds with **`402 Payment Required`** and a structured body. No work is performed and nothing is charged.

```json theme={null}
{
  "code": "INSUFFICIENT_CREDITS",
  "message": "Insufficient credits: have 3, need 10",
  "balance": 3,
  "required": 10
}
```

| Field      | Meaning                                                                       |
| ---------- | ----------------------------------------------------------------------------- |
| `code`     | Always `INSUFFICIENT_CREDITS` — switch on this rather than parsing `message`. |
| `balance`  | Your current balance.                                                         |
| `required` | Tokens needed for this request (worst case, for sourcing searches).           |

**How to handle it:**

<Steps>
  <Step title="Detect">
    Treat any `402` with `code: "INSUFFICIENT_CREDITS"` as a billing condition, not a transient error — retrying without adding tokens will fail again.
  </Step>

  <Step title="Top up">
    Add tokens in the [Fribl Console](https://console.fribl.co). You can confirm the new balance with `GET /credits/balance`.
  </Step>

  <Step title="Retry">
    Re-send the original request. Because charging is idempotent, it's safe to retry the exact same payload.
  </Step>
</Steps>

## Trial tokens

New workspaces may start with a one-time grant of trial tokens so you can evaluate the API before purchasing. Trial grants can carry an expiry date; when they expire, the unused portion is removed and recorded in your history as a `token_expired` entry. Check `GET /credits/balance` to see what you currently have available.

## Next steps

<CardGroup cols={2}>
  <Card title="Quickstart" icon="bolt" href="/quickstart">
    Run your first analyze-and-match flow end to end.
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation" href="/errors">
    See every status code, including `402` for insufficient tokens.
  </Card>
</CardGroup>
