Payment intents, legs and custody
A payment along an approved quote, the legs it is made of, and who holds the money at each step.
A payment intent (pi_…) is a payment along a quote your user approved. You create it, help your user through any action it needs, and then listen: Unirail drives the legs with provider webhooks, polling and status deadlines, and tells you how it ends.
const intent = await unirail.paymentIntents.create(
{
quote: quote.id,
digest: quote.digest,
returnUri: "https://example.com/pay/done",
reference: "Dinner",
psu: { ip: request.ip, userAgent: request.headers.get("user-agent") ?? undefined },
metadata: { paymentId: payment.id },
},
{ context: { idempotencyKey: `payment:${payment.id}` } },
);psu forwards context from your user's device; some banks require it. metadata comes back on every event about the intent, so you can find your own record.
Status
status | Meaning |
|---|---|
requires_action | Your user has to do something: see nextAction (user actions). |
processing | Submitted; Unirail is following the legs. |
succeeded | The payment completed. |
failed | It didn't work; failure explains why. |
cancelled | It was cancelled, for example with paymentIntents.cancel. |
After a redirect or SDK step, call paymentIntents.advance({ id, returnParams }) or paymentIntents.advance({ id, sdkResult }). Each status change also arrives as a payment_intent.* event.
Legs
A payment is made of one or more legs, in sequence. Each leg is one hop on one rail:
| Field | Meaning |
|---|---|
from, to | The accounts at each end. |
rail | The provider that moves this hop, such as plaid. |
amount | What this hop moves. |
custodyAfter | Who holds the money once this hop completes. |
state | Where the hop is (below). |
providerRef | The provider's own reference, for support conversations with that provider. |
Leg states run from created through the waiting states (awaiting-user, awaiting-step-up, awaiting-submit, awaiting-funds, blocked-on-information) to submitted, accepted, confirming and credited. They can end in failed, cancelled, refunded or returned. Rails with no completion signal end in unconfirmed or attested rather than credited. leg.updated fires on every change.
Custody
Custody is data, not an assumption. Every leg says who holds the money after it:
custodyAfter | Who holds the money |
|---|---|
none | Nobody. The payer's bank pays the payee's bank directly. |
payer-own-account | The payer, in an account they already hold with the provider. |
provider-for-user | The provider, in an account or wallet in the user's name. |
provider-transit | The provider, between collecting from the payer and paying the payee. |
platform-entity | Your company, in an account it holds. |
partner-entity | A partner, in an account it holds on your behalf. |
Your routing policy lists the custody kinds each environment admits. A route whose legs need anything else is returned as infeasible, so a platform that must never touch funds can say so in policy and have Unirail enforce it.