Authorization lifecycle
A payment authorization records that funds have been reserved on a customer's payment method. In Hantera, authorizations are a system of record: they reflect the truth of the underlying payment provider, they don't dictate it. The platform never assumes it can change what a provider has reserved — it records what the provider reports.
States
An authorization has an authorizationState:
| State | Meaning |
|---|---|
pending | The authorization has been requested but not yet confirmed by the provider. |
successful | The provider confirmed the reservation. The amount reflects the reserved funds. |
failed | A pending authorization that the provider ultimately rejected. |
The amount is the system of record for money. It mirrors the provider throughout the authorization's life:
- It goes up when a reservation is recorded (create).
- It goes down as funds are captured (draw-down).
- It goes to zero when the reservation is released (release).
A successful authorization with amount > 0 is an outstanding reservation. When a provider releases or voids a reservation, the amount is set to 0 — the state stays successful, because the authorization did succeed; there is simply nothing left reserved.
Recording an authorization
Record an authorization with the createAuthorization command once a provider has reserved funds. The authorizationNumber is the natural key for the authorization (unique within the payment) — use the provider's own reservation/authorization id so every later operation can address it.
from {
type = 'applyCommands'
body = {
commands = [{
type = 'createAuthorization'
authorizationNumber = 'AUTH-123' // the provider's reservation id
amount = 499.00
}]
}
}authorizationState defaults to successful. For two-phase providers — where a reservation is requested and confirmed asynchronously — record it as pending first and move it to successful (or failed) with setAuthorizationState once the provider responds:
// On the provider's confirmation webhook:
from {
type = 'applyCommands'
body = {
commands = [{
type = 'setAuthorizationState'
authorizationNumber = 'AUTH-123'
authorizationState = 'successful'
}]
}
}Drawing down an authorization
As funds are captured against a reservation, the authorized amount should shrink to reflect what is still reserved. The charge message does this automatically: pass the authorizationNumber and the charge reduces that authorization's amount by the charged amount.
// After the provider confirms a capture against the authorization:
from messageActor(
'payment'
paymentId
[{
type = 'charge'
body = {
amount = 199.00
authorizationNumber = 'AUTH-123' // reduces AUTH-123 by 199.00
transactionReference = 'CAPTURE-456'
}
}]
)The charge is rejected if the authorization is not successful or if the charged amount exceeds the remaining authorized amount — the reservation can never be over-drawn. A partial capture leaves a smaller successful authorization behind; the release flow then frees whatever remains.
For direct reconciliation — for example when syncing the authoritative state back from the provider — use setAuthorizationAmount and setAuthorizationState to bring the Hantera record in line with provider truth.
Releasing an authorization
When an order is cancelled (or an operator chooses to free a reservation), the outstanding authorization should be released with the provider. Because authorizations follow provider truth rather than dictating it, releasing is modelled as a request — the payment.releaseAuthorization message — rather than a direct state mutation.
releaseAuthorization does not touch the authorization itself. It evaluates the OnPaymentAuthorizationRelease hook and lets the responsible app decide how to carry out the release.
The OnPaymentAuthorizationRelease hook
The hook input is:
{
hook: 'OnPaymentAuthorizationRelease'
payment: Payment
authorization: PaymentAuthorization
}Supported effects: payment.command, common.scheduleJob, common.sendMessage, common.sendEmail.
Effect-resolution contract
After the hook runs, the actor resolves the outcome by what the rules emitted:
- A rule emits one or more
payment.commandeffects — the commands are applied to the payment. The rule owns the outcome (e.g. it emitssetAuthorizationAmount = 0directly). Use this for simple integrations with no external system to call. - A rule emits a job, message, or email (but no command) — an external system has taken responsibility for the release. The provider's job voids the reservation and then zeroes the authorization once confirmed. The actor leaves the amount untouched in the meantime, accepting a brief, in-scope mismatch between Hantera and the provider.
- No rule emits anything at all — nobody took responsibility, so the actor optimistically sets the authorization amount to
0. This is the correct default when there is no external system to validate the release (for example in-house invoicing): a reservation that nobody manages no longer reflects reality.
This single contract — "if nobody took responsibility for the release, assume it succeeded and zero it" — means the trivial case needs zero configuration, while PSP-backed payments converge their external call through the same message.
Implementing a PSP release rule
A payment provider app reacts to the hook by matching its providerKey and scheduling its release job:
param input: OnPaymentAuthorizationRelease
from
input match
when input.payment.providerKey == 'myprovider' and input.authorization.amount > 0 |> {
effect = 'scheduleJob'
definition = 'apps/myprovider/releaseAuthorization'
at = now
parameters = {
paymentId = input.payment.paymentId
}
}The scheduled job voids the reservation with the provider and, on success, records the truth on the Hantera payment by setting the authorization amount to 0.
In-house invoicing
When there is no external provider, a one-line tenant rule brings the authorization to truth directly:
param input: OnPaymentAuthorizationRelease
from {
command = 'setAuthorizationAmount'
authorizationNumber = input.authorization.authorizationNumber
amount = 0
}Or omit the rule entirely and rely on the optimistic auto-zero (resolution branch 3 above).
Manual vs automated release
The same releaseAuthorization message drives every release path, so providers react identically whether the release was automated or operator-initiated:
- Automated — Hantera Commerce's cancel-order flow sends
releaseAuthorizationfor each outstanding authorization when a customer cancels. - Manual — an operator clicking "Release authorization" in the portal sends the same message.
The two are distinguished by the actor identity recorded on the resulting mutation and activity log, not by any branching in the rule.