Grant Credits
POST /io/wallet/grant — mint non-payment credit (plan perks, promos) idempotently. Service-key only, fail-closed.
Grants a non-payment credit bonus to a wallet — a subscription plan's monthly wallet_credit_monthly perk, a promotional bonus, a support credit. Unlike a top-up, no money changes hands: the credits are minted directly into the ledger.
POST https://api.misar.io/io/wallet/grantService key only — never expose this to end users
This endpoint mints free credit. It requires x-wallet-service-key and rejects SSO bearer tokens outright. There is no user-facing auth mode.
Authentication
x-wallet-service-key: <WALLET_SERVICE_KEY>
Content-Type: application/json/io/wallet/grantMints credits into the wallet of user_id, keyed on idempotency_key so a retried webhook or a duplicate call for the same billing period never double-grants.
Request body
user_idstringbodyrequiredThe Misar SSO user id to credit. Must be a valid UUID.
creditsnumberbodyrequiredHow many credits to mint. Must be a finite number greater than 0. Rounded to 4 decimal places server-side.
idempotency_keystringbodyrequiredA stable key that identifies this grant. Re-sending the same key returns the original outcome with idempotent: true instead of granting again. Use something period-scoped, e.g. plan-perk:mail-pro:2026-08.
productstringbodyOptional product slug recorded on the ledger entry (mail, blog, reach, …). Defaults to null.
Response fields
successbooleantrue only when credits were minted (or the grant was already applied under this idempotency_key). Anything else is false.
balancenumberThe wallet balance after the grant. Omitted when the grant did not run.
idempotentbooleantrue when this idempotency_key had already been applied and no new credit was minted.
curl -X POST "https://api.misar.io/io/wallet/grant" \
-H "x-wallet-service-key: $WALLET_SERVICE_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "3f1a2b4c-5d6e-4f70-8a91-b2c3d4e5f607",
"credits": 25,
"idempotency_key": "plan-perk:mail-pro:2026-08",
"product": "mail"
}'{
"success": true,
"balance": 67,
"idempotent": false
}{
"success": true,
"balance": 67,
"idempotent": true
}{
"success": false
}Status codes
| Code | Meaning |
|---|---|
200 | The grant ran. Read success — a 200 with success: false means the ledger call failed. |
400 | Malformed JSON, missing or non-UUID user_id, credits not a positive finite number, or empty idempotency_key. |
401 | Missing or invalid x-wallet-service-key. |
429 | Wallet write rate limit exceeded (50 write ops/user/60 s). |
Rate limit
50 write ops per user per 60 s, shared with the other wallet write endpoints. Over the limit returns 429 with { "success": false }.
Fail-closed behaviour
A failure never silently succeeds
If the ledger RPC errors or returns a malformed row, the response is 200 with { "success": false } — deliberately, so a caller cannot mistake an outage for a completed grant. Retry with the same idempotency_key; the ledger will apply the grant at most once regardless of how many times you retry.
Grant vs. top-up vs. earn
| Endpoint | Money involved | Typical caller |
|---|---|---|
/grant | None — credit is minted | Billing webhook applying a plan perk |
/topup-session | Yes — Stripe Checkout | The user's own browser |
/earn | None — credit is earned from platform activity | Product backend recording a payout-eligible action |