Skip to content

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.

POST/api/v1/wallets/{id}/grants

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

FieldTypeDescription
idwal_… IDThe wallet (wal_…).

Body

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

FieldTypeRequiredDescription
sourceone of promo, adjustmentYespromo: promotional credits (spent first, never refundable). adjustment: a goodwill credit spent with purchased credits.
amountobjectYesThe credits to grant, in the wallet's currency.
amount.amountstringYesInteger micros as a string (5000000 = 5.00).
amount.currencystringYesISO 4217 currency code: the wallet's currency.
reasonstringYesWhy the credits are granted; shown on the grant, in the ledger and in the audit log.
expiresAtdate-timeNoWhen unused credits expire (ISO 8601, in the future); they never expire without it.

Response

201 Created with a JSON object:

FieldTypeDescription
idgrant_… ID
object"credit_grant"
brandIdbrd_… ID
livemodeboolean
walletIdwal_… ID
sourceone of purchase, subscription, promo, adjustment, refund_reversalpurchase, subscription, promo, adjustment or refund_reversal.
amountmoneyWhat was granted.
remainingmoneyWhat is left: not yet spent or expired (reserved credits count as left).
priorityintegerSpend tier (§17.1): 1 promo, 2 subscription, 3 purchased and the rest.
expiresAtdate-time or null
expiredbooleanexpiresAt has passed: the credits can't be spent any more.
reasonstring or nullThe operator's reason, for promo and adjustment grants.
paymentIdstring or null
invoiceIdstring or null
createdAtdate-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

CodeHTTP statusWhenWhat to do
validation_failed400The 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_found404This 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_mismatch400The 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_closed409The 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_reused409The idempotency key was used before for another request.Use a new Idempotency-Key for a different request, or resend the original request unchanged.
version_conflict409The wallet changed at the same moment (another reservation, capture or grant); retry.Fetch the object again and retry the change.
approval_required202The 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_rejected409The 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_expired409The 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_approval409The 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_required403Your last sign-in is older than 15 minutes.Sign in again, then repeat the action within 15 minutes.
scope_required400The 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_denied403You 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_allowed403You 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: api_version_unsupported, invalid_json, unsupported_media_type, authentication_required, api_key_invalid, idempotency_key_required, idempotency_in_progress, approval_policy_denied, rate_limited, internal_error.