# Grant promo or adjustment credits

> The credit_grants.create operation: POST /api/v1/wallets/{id}/grants in the REST API and the credit_grants_create tool in the MCP server.

Grants credits to a wallet without a payment, with a reason:

- `promo`: promotional credits, spent before any other and never refundable;
- `adjustment`: a goodwill credit, spent with purchased credits.

In the ledger the credits are a cost (`promo_expense`); unused promo and adjustment credits that expire reverse
it. `expiresAt` (optional) is when unused credits expire. A closed wallet takes no grants (`wallet_closed`); a
frozen one does.

Financial tier: a sign-in in the last 15 minutes for people, and an approval policy may require a second person
above an amount. Dry-run it first. Emits `wallet.credited`.

## At a glance

- **REST:** `POST /api/v1/wallets/{id}/grants`
- **MCP tool:** `credit_grants_create`
- **Operation ID:** `credit_grants.create`
- **Scope:** Brand
- **Permission:** `wallets:write`
- **Risk tier:** `financial`
- **Dry run:** Supported (send `Ownrecord-Dry-Run: true`)
- **Idempotency:** `Idempotency-Key` required
- **Modes:** Test and live
- **Authentication:** API key with the `wallets:write` scope
- **Fresh sign-in:** In the console, a sign-in within the last 15 minutes
- **Since:** M6

## Request

### Path parameters

| Field | Type       | Description           |
| ----- | ---------- | --------------------- |
| `id`  | `wal_…` ID | The wallet (`wal_…`). |

### Body

A JSON object (`Content-Type: application/json`):

| Field             | Type                         | Required | Description                                                                                                                 |
| ----------------- | ---------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |
| `source`          | one of `promo`, `adjustment` | Yes      | `promo`: promotional credits (spent first, never refundable). `adjustment`: a goodwill credit spent with purchased credits. |
| `amount`          | object                       | Yes      | The credits to grant, in the wallet's currency.                                                                             |
| `amount.amount`   | string                       | Yes      | Integer micros as a string (5000000 = 5.00).                                                                                |
| `amount.currency` | string                       | Yes      | ISO 4217 currency code: the wallet's currency.                                                                              |
| `reason`          | string                       | Yes      | Why the credits are granted; shown on the grant, in the ledger and in the audit log.                                        |
| `expiresAt`       | date-time                    | No       | When unused credits expire (ISO 8601, in the future); they never expire without it.                                         |

## Response

`201 Created` with a JSON object:

| Field       | Type                                                                        | Description                                                              |
| ----------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `id`        | `grant_…` ID                                                                |                                                                          |
| `object`    | `"credit_grant"`                                                            |                                                                          |
| `brandId`   | `brd_…` ID                                                                  |                                                                          |
| `livemode`  | boolean                                                                     |                                                                          |
| `walletId`  | `wal_…` ID                                                                  |                                                                          |
| `source`    | one of `purchase`, `subscription`, `promo`, `adjustment`, `refund_reversal` | `purchase`, `subscription`, `promo`, `adjustment` or `refund_reversal`.  |
| `amount`    | money                                                                       | What was granted.                                                        |
| `remaining` | money                                                                       | What is left: not yet spent or expired (reserved credits count as left). |
| `priority`  | integer                                                                     | Spend tier (§17.1): 1 promo, 2 subscription, 3 purchased and the rest.   |
| `expiresAt` | date-time or null                                                           |                                                                          |
| `expired`   | boolean                                                                     | `expiresAt` has passed: the credits can't be spent any more.             |
| `reason`    | string or null                                                              | The operator's reason, for promo and adjustment grants.                  |
| `paymentId` | string or null                                                              |                                                                          |
| `invoiceId` | string or null                                                              |                                                                          |
| `createdAt` | date-time                                                                   |                                                                          |

## Examples

### €5 of welcome credits for a month

```http
POST /api/v1/wallets/wal_01m4ty7w2a5qx7mhd9k8w6s0b1/grants
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

{
  "source": "promo",
  "amount": {
    "amount": "5000000",
    "currency": "EUR"
  },
  "reason": "Welcome credits",
  "expiresAt": "2026-11-01T00:00:00Z"
}
```

Response (`201`):

```json
{
  "id": "grant_01m4ty7w2a5qx7mhd9k8w6s0b3",
  "object": "credit_grant",
  "brandId": "brd_01m4tx5nj8emwbyf7y9vcf47x4",
  "livemode": false,
  "walletId": "wal_01m4ty7w2a5qx7mhd9k8w6s0b1",
  "source": "promo",
  "amount": {
    "amount": "5000000",
    "currency": "EUR"
  },
  "remaining": {
    "amount": "5000000",
    "currency": "EUR"
  },
  "priority": 1,
  "expiresAt": "2026-11-01T00:00:00.000Z",
  "expired": false,
  "reason": "Welcome credits",
  "paymentId": null,
  "invoiceId": null,
  "createdAt": "2026-10-16T10:15:00.000Z"
}
```

## Errors

| Code                                                                                                 | HTTP status | When                                                                                                                                      | What to do                                                                                                                                                                                                                                                                            |
| ---------------------------------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed)               | 400         | The amount isn't positive, the reason is missing or too long, `source` isn't `promo` or `adjustment`, or `expiresAt` isn't in the future. | Fix the fields listed in `details.issues` and send the request again.                                                                                                                                                                                                                 |
| [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found)             | 404         | This brand has no such wallet in this mode (wallets belong to one brand and one mode).                                                    | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode.                                                                                                                                                                                  |
| [`wallet_currency_mismatch`](https://ownrecord.io/docs/reference/errors.md#wallet_currency_mismatch) | 400         | The amount isn't in the wallet's currency.                                                                                                | Use the wallet for this currency. A customer has one wallet per currency (`details.expected`, `details.actual`).                                                                                                                                                                      |
| [`wallet_closed`](https://ownrecord.io/docs/reference/errors.md#wallet_closed)                       | 409         | The wallet is closed: reopen it before granting credits.                                                                                  | Reopen the wallet with `wallets.update` (`status: active`). A purchase of credits in its currency reopens it, too.                                                                                                                                                                    |
| [`idempotency_key_reused`](https://ownrecord.io/docs/reference/errors.md#idempotency_key_reused)     | 409         | The idempotency key was used before for another request.                                                                                  | Use a new Idempotency-Key for a different request, or resend the original request unchanged.                                                                                                                                                                                          |
| [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict)                 | 409         | The wallet changed at the same moment (another reservation, capture or grant); retry.                                                     | Fetch the object again and retry the change.                                                                                                                                                                                                                                          |
| [`approval_required`](https://ownrecord.io/docs/reference/errors.md#approval_required)               | 202         | The organization's approval policy needs a second person for this; nothing changed yet.                                                   | Nothing ran yet. An approver reviews `approval.summary` and the dry-run result in the console (`approval.approveUrl`). Poll `GET /approvals/{id}` or watch for the `approval.resolved` event; sending the same request again with the same Idempotency-Key returns the same approval. |
| [`approval_rejected`](https://ownrecord.io/docs/reference/errors.md#approval_rejected)               | 409         | The same request was sent again after it was rejected.                                                                                    | Read the reason in `details.approval.rejection`. To try again, send a new request with a new Idempotency-Key.                                                                                                                                                                         |
| [`approval_expired`](https://ownrecord.io/docs/reference/errors.md#approval_expired)                 | 409         | The same request was sent again after its approval request expired.                                                                       | Send the request again with a new Idempotency-Key to create a new approval request. Requests expire after 72 hours.                                                                                                                                                                   |
| [`stale_approval`](https://ownrecord.io/docs/reference/errors.md#stale_approval)                     | 409         | The approved request could no longer run as requested.                                                                                    | Check the current state (`details.approval.failure` says what failed), then send a new request with a new Idempotency-Key.                                                                                                                                                            |
| [`fresh_auth_required`](https://ownrecord.io/docs/reference/errors.md#fresh_auth_required)           | 403         | Your last sign-in is older than 15 minutes.                                                                                               | Sign in again, then repeat the action within 15 minutes.                                                                                                                                                                                                                              |
| [`scope_required`](https://ownrecord.io/docs/reference/errors.md#scope_required)                     | 400         | The request names no brand (an org or entity key without `Ownrecord-Brand`).                                                              | Call the operation with credentials or a target at its level: an organization, a legal entity, a brand or the platform (`details.required_scope`).                                                                                                                                    |
| [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied)               | 403         | You can't change this brand's wallets (`wallets:write`: owners, admins and support).                                                      | Ask an organization owner or admin for a role or scope that allows this action.                                                                                                                                                                                                       |
| [`livemode_not_allowed`](https://ownrecord.io/docs/reference/errors.md#livemode_not_allowed)         | 403         | You have no live-mode access, so you can't use live wallets.                                                                              | Use test mode, or credentials for the target's mode. Developers need live access granted by an organization owner or admin.                                                                                                                                                           |

It can also return these [common errors](https://ownrecord.io/docs/reference.md#common-errors): [`api_version_unsupported`](https://ownrecord.io/docs/reference/errors.md#api_version_unsupported), [`invalid_json`](https://ownrecord.io/docs/reference/errors.md#invalid_json), [`unsupported_media_type`](https://ownrecord.io/docs/reference/errors.md#unsupported_media_type), [`authentication_required`](https://ownrecord.io/docs/reference/errors.md#authentication_required), [`api_key_invalid`](https://ownrecord.io/docs/reference/errors.md#api_key_invalid), [`idempotency_key_required`](https://ownrecord.io/docs/reference/errors.md#idempotency_key_required), [`idempotency_in_progress`](https://ownrecord.io/docs/reference/errors.md#idempotency_in_progress), [`approval_policy_denied`](https://ownrecord.io/docs/reference/errors.md#approval_policy_denied), [`rate_limited`](https://ownrecord.io/docs/reference/errors.md#rate_limited), [`internal_error`](https://ownrecord.io/docs/reference/errors.md#internal_error).
