Adjust
Credit or debit a participant’s balance:
Amounts must be positive. Use
type to control direction. DEBIT fails if the available balance is insufficient, unless allow_negative is set to true. If amount has more decimal places than the asset’s scale, the request is rejected with a 400 whose details.reason is invalid_scale. Round in your client before calling adjust.
Each adjustment creates one journal entry. Its action_type is CREDIT or DEBIT, matching type; there is no separate adjustment action type. To tell a manual adjustment from a rule’s credit or debit, check event_id and rule_id. Both are null on an adjustment, and both are set on an entry a rule created.
Expiration and maturity on credits
A manualCREDIT for a LOT-mode asset can set expires_at and matures_at. This applies to participant and group adjustments.
Use an RFC 3339 timestamp or a positive duration for either field. Durations count from the same request time. If you set both, the resolved maturity must be before expiration. A future matures_at places the credit in DEFERRED until it matures.
Scrip rejects these fields on a DEBIT, a SIMPLE-mode asset, or a credit to HELD.
On a retry, send the same idempotency key and the same timestamp or duration strings. Scrip compares the strings you sent and returns the original journal entry without recalculating durations. See Idempotency.
Negative balances
Setallow_negative to true on a DEBIT to let the balance go below zero. This is useful when you need to recover value that a participant has already spent:
- Refund clawbacks. A cardholder earned cashback on a purchase that was later refunded. The cashback has already been redeemed, so the available balance is zero. A negative-balance debit records the debt.
- Chargeback recovery. A payment is disputed and reversed, but the associated reward points were already used. The debit brings the balance negative until the participant earns enough to offset it.
- Corrections. An incorrect credit was issued and the participant has already spent part of it. A negative-balance debit corrects the ledger without waiting for funds to replenish.
LOT-mode assets, the system consumes whatever lots are available first, then posts the remaining amount as an overdraft. The balance goes negative by the uncovered portion.
allow_negative also works in rule actions. This lets your rules engine handle clawbacks automatically, for example debiting cashback when a refund event arrives, even if the participant’s balance is zero.
allow_negative only applies to adjustments and rule-based debits. Transfers and redemptions always require sufficient funds.Hold
Reserve funds by moving them fromAVAILABLE to HELD:
Held funds are not spendable. Use holds for:
- Authorization holds (reserve rewards until the transaction settles)
- Fraud review (freeze funds pending investigation)
- Pending approvals (hold until manual review completes)
Release
Move held funds back toAVAILABLE:
When
reference_id is provided, only lots stamped with that reference during a previous hold are targeted. You can also filter by lot age using earned_from and earned_to (RFC 3339 timestamps) for batch-releasing held balances. LOT mode only.
Forfeit
Remove funds permanently from a participant’s balance:
Forfeited funds move to the
SYSTEM_BREAKAGE account.
Void hold
Cancel HELD lots that were credited directly into HELD (e.g., pending authorization rewards) byreference_id, returning value to the original source account:
Use void hold when an authorization is reversed before settlement. It cancels pending rewards credited directly into
HELD and returns the value to the program wallet for PREFUNDED assets or SYSTEM_ISSUANCE for UNLIMITED assets.
Void hold only processes lots that were created directly in HELD via CREDIT. Lots moved to HELD via a HOLD operation (participant funds) are excluded for safety: returning those to the source would effectively confiscate participant value. Use release to return participant-held funds to AVAILABLE.
amount when the issuer processor reverses only part of an authorization. amount is the rewards to cancel, not the purchase amount that was reversed. See how to calculate amount.
amount_processed in the response shows how much was voided. The amount in the balance.voided webhook matches it. Both can be less than the amount you sent.
Scrip voids the newest pending rewards first because a partial reversal usually undoes the most recent authorization for the reference. A lot that is only partly voided keeps its reference_id, expires_at, and matures_at. The settlement or a later void still finds it.
lot_ids, earned_from, and earned_to are not supported.
Auth / settlement pattern
Card purchases happen in two steps. The issuer processor authorizes the purchase first, and the merchant settles (captures) it later, sometimes days later. The settled amount can differ from the authorized amount because of tips, partial captures, or currency conversion. Most programs skip authorizations and award rewards only at settlement. That is the simplest approach, and the Stripe Issuing example uses it. If you want cardholders to see pending rewards as soon as they tap their card, send both events to Scrip. Pending rewards are credits made straight intoHELD at authorization. At settlement, Scrip adjusts them to the settled amount.
This pattern requires a
LOT-mode asset. reference_id labels the lots created at authorization so the settlement can find them. For SIMPLE-mode assets, use holds and releases by amount, without reference_id.1
Authorization: add pending rewards
When a cardholder taps their card, your issuer processor sends an authorization webhook. Your backend forwards it to Scrip as an event with The cardholder’s Finish an authorization that used
event.type == "auth". A rule credits the rewards into the HELD bucket, with the authorization ID as their reference_id:held balance now shows these pending rewards. They cannot be spent yet.To reserve part of the participant’s existing balance instead of adding new rewards, use HOLD:HOLD with RELEASE. A settlement CREDIT fails with reference_provenance_conflict when the reference has balance reserved with HOLD.2
Settlement: confirm the rewards
When the merchant captures the transaction, your backend sends a second event with Scrip replaces the pending rewards with the settled amount. See Auto-reconciliation.Release. If the settled amount always equals the authorized amount, Without
event.type == "settlement". Choose one of two approaches.Credit with the same reference. Send a CREDIT with the authorization’s reference_id and no bucket:RELEASE is enough. It moves the pending rewards to AVAILABLE unchanged:amount, RELEASE moves every lot held under that reference_id.3
Or: void the authorization
If the authorization is reversed before it settles, send If the issuer processor reverses only part of the authorization, add The void rule passes the reward difference as
VOID_HOLD to cancel the pending rewards. The value goes back to where it came from: the program wallet or system issuance.amount. Scrip cancels that much and leaves the rest for the settlement. amount is the rewards to cancel, in the asset’s units. It is not the purchase amount that was reversed.Calculate amount as the reward difference: the rewards for the amount authorized before this reversal minus the rewards for the amount still authorized. Use the earn terms that applied at authorization. Running the authorization CREDIT formula on the reversed purchase amount gives the right result only when rewards are a flat percentage of the purchase.For example, your authorization rule credits 0.03 points per dollar, plus a 5-point bonus on purchases over $75. A $100 authorization adds 8 pending points. The merchant then reverses $50. The $50 that stays authorized earns 1.5 points, so amount is 6.5. The formula applied to the reversed $50 gives 1.5, which would leave 6.5 points pending instead of 1.5.Incremental authorizations and earlier partial reversals change the amount authorized before this reversal. Start from that amount, not from the first authorization.Have your backend send the reward difference on the reversal event, next to the purchase amounts:amount:amount must be a positive number. A blank amount is rejected, and an expression that resolves to blank fails the action. Neither cancels all pending rewards. To cancel all of them, leave amount out.Some authorizations never settle and are never reversed. Set expires_at on the authorization CREDIT, for example "720h", so their pending rewards do not stay forever. When expires_at passes, they are forfeited to breakage. Send VOID_HOLD whenever you know an authorization is gone, because it cancels the rewards instead of counting them as breakage.See the Stripe Issuing example for authorizations that are never captured.Auto-reconciliation
ACREDIT with a reference_id and no bucket settles the pending rewards for that reference. Scrip compares the settled amount with the pending amount and adjusts:
With a future
matures_at on the settlement, each row lands in DEFERRED instead of AVAILABLE. See Delay when settled rewards can be spent.
Settlement journal entry
When a settlement finds pending rewards, it creates oneCREDIT journal entry with the authorization’s reference_id and up to three postings:
HELD posting always removes the full pending amount. The destination posting, AVAILABLE or DEFERRED, adds the settled amount. The third posting appears only when the two amounts differ. When the settlement is less than the pending rewards (an under-capture), the difference goes back to the program wallet or SYSTEM_ISSUANCE. When it is more (an over-capture), the extra comes from there.
In a statement with bucket=HELD, the settlement appears as a negative CREDIT row with the same reference_id as the authorization credit.
Things to know about settlement:
- Several authorizations, one reference. When incremental authorizations share a
reference_id, the settlement is compared with their total. An authorization that arrives after the reference has settled adds no pending rewards. - One reference per card transaction. Use a
reference_idthat is unique to each card transaction. Once a reference has settled, later authorizations with the samereference_idadd no pending rewards. The next settlement for that reference still credits its full amount. - Expired pending rewards. Pending rewards whose
expires_athas passed are left out. - Dates come from the settlement. The settled rewards use the settlement’s
expires_atandmatures_at. The authorization’sexpires_atdoes not carry over. - Leave
bucketunset. Before the reference has settled, a settlementCREDITwith"bucket": "HELD"adds more pending rewards instead of settling. A laterVOID_HOLDfor the reference then cancels those too. After the reference has settled, Scrip skips aHELDcredit for it. - Put
matures_aton the settlement only. Pending rewards with a futurematures_atare skipped at settlement and stay inHELD. The settlement then credits its full amount, so the balance counts the purchase twice until the pending rewards are voided or expire.
Delay when settled rewards can be spent
Addmatures_at to the settlement CREDIT to keep settled rewards from being spent until a date you choose, such as the end of the cardholder’s billing cycle. The settlement works the same way, but the rewards go to DEFERRED instead of AVAILABLE:
matures_at passes, the rewards move to AVAILABLE. Scrip then sends a balance.matured webhook. If matures_at has already passed when Scrip processes the settlement, the rewards go straight to AVAILABLE. See Vesting.
With this setup, each balance field shows one stage of the purchase:
Out-of-order and repeated events
Issuer processors do not always send webhooks in order, and one authorization can be reversed or captured more than once. The table shows what Scrip does in each case and how to handle it.reference_id only links a settlement to its authorization. It does not stop duplicate events. For that, build each event’s idempotency_key from the issuer processor’s ID for that event.
When to use this pattern
Idempotency
All balance operations accept an idempotency key so retries never double-apply. Pass it either as anidempotency_key field in the body or as the standard Idempotency-Key HTTP header. The two are equivalent. If both are provided they must match; mismatched values return a 400 (header_body_mismatch) rather than silently picking one. Header values must be 1-255 printable ASCII characters.
Replaying the same key with identical parameters returns the original result without creating new postings. The same key with different parameters returns 409 (idempotency_conflict). See Idempotency in the API reference for the full contract.
Lot preservation
ForLOT-mode assets, hold and release preserve each lot’s metadata and identity across bucket transitions. See Lot-Aware Operations for the full behavior.
Inactive participants
Most balance operations (adjust, hold, release) are blocked forSUSPENDED and CLOSED participants. The API returns a 409 error with code participant_inactive.
Forfeit and void hold are the exceptions: they are allowed on CLOSED participants so you can clean up remaining balances after account closure. Both are still blocked for SUSPENDED participants.