Payment Lifecycle and Confirm
Understand payment creation, confirm, requires_action, succeeded, authorized, and charge attempt relationships in HSRC Pay.
This page explains the difference between creating a Payment and confirming a Payment, why Confirm is kept separate, and how one Payment can produce multiple Charge attempts.
Confirm is the controlled transition that moves the payment record into real provider execution.
What is payment lifecycle?
Payment lifecycle is the state model from merchant payment intent creation to terminal outcome. In HSRC Pay, the important point is: Payment is the business goal; Charge is the attempt at provider level.
Public API status matches the Prisma PaymentStatus enum (uppercase, not snake_case):
| Status | Meaning |
|---|---|
CREATED | Payment record created; confirm has not run yet |
REQUIRES_PAYMENT_METHOD | Method missing or last confirm failed/declined; can confirm again |
REQUIRES_ACTION | 3DS, redirect, or user action required |
PROCESSING | Provider execution or async result pending |
AUTHORIZED | Auth-only success (auto_capture: false) |
SUCCEEDED | Collection completed (capture included when auto_capture: true) |
CANCELED | Transaction canceled |
REFUNDED | Refund processed after successful payment |
EXPIRED | Invalid due to expiry or business rule |
Decline and technical error do not produce separate DECLINED / FAILED status on Payment; when orchestrator result is declined or error, Payment is usually REQUIRES_PAYMENT_METHOD. Decline detail lives on the related Charge record (DECLINED, FAILED).
Statuses that can be confirmed again before confirm: CREATED, REQUIRES_ACTION, REQUIRES_PAYMENT_METHOD.
Create vs Confirm separation
Creating a Payment does not mean pulling funds. Create prepares merchant intent, amount, currency, auto_capture, secure_mode, customer context, and metadata; the record opens as CREATED.
Confirm starts routing plan and provider execution (charge attempt) with payment_method, optional routing_strategy, and payer_identity.
| Stage | What does it do? | Money movement? |
|---|---|---|
| Create Payment | Prepares business intent and payment record | No |
| Confirm Payment | Routing plan, charge attempt, provider adapter execution | Yes in production, simulated in sandbox |
What does Confirm do?
At confirm time, typically:
- Payment is checked for confirmable status.
- Authorization (
payment:confirm) and account boundary are validated. - Payment Method is normalized (token, saved method, or inline card data).
- Candidate pool is resolved from account routing policy or request
routing_strategy. - Routing plan is built; for each candidate a Charge is created and provider adapter
payruns. - Result returns as normalized adapter result; Payment status and optional
next_actionupdate. - Charge status updates (e.g.
CAPTURED,AUTHORIZED,REQUIRES_ACTION,DECLINED). - Domain event and webhook delivery fire.
Confirm HTTP response
Confirm does not return a single kind field. Standard v1 envelope includes data.payment (current Payment DTO) and data.confirmResult (orchestrator summary).
{
"data": {
"payment": {
"object": "payment",
"id": "pay_123",
"status": "REQUIRES_ACTION",
"amount": 12500,
"currency": "TRY"
},
"confirmResult": {
"ok": true,
"results": [],
"latestResult": { "kind": "requires_action" },
"paymentNextAction": {
"action": "redirect_user_to_url",
"url": "https://..."
}
}
}
}| Field | Merchant behavior |
|---|---|
payment.status | Order / workflow state; reconcile with webhook |
confirmResult.ok | true when last step is success or requires_action |
confirmResult.latestResult | Provider adapter summary (raw nextAction may appear here) |
confirmResult.paymentNextAction | Platform next_action: currently redirect_user_to_url + url |
Successful confirm with auto_capture: false sets Payment to AUTHORIZED; capture via separate endpoint targets SUCCEEDED / charge CAPTURED.
Requires Action / 3DS
When secure mode or provider requirements need extra user action, Confirm may not return SUCCEEDED / AUTHORIZED immediately. Payment becomes REQUIRES_ACTION; use confirmResult.paymentNextAction (and payment.next_action on the record) for redirect on the merchant side.
Hosted Checkout can manage this action inside the user experience. In API-only integration the merchant redirects with paymentNextAction.url; after challenge the gateway callback URL (linked to charge) completes the resume flow.
Provider adapter nextAction (e.g. render_html) and platform paymentNextAction are different layers; use the platform field for user redirect in integration.
Resume / 3DS callback
Resume is not a separate public payment endpoint named resumePayment(paymentId). When 3DS completes, the provider returns to HSRC Pay gateway at three-d-secure-callback-urls/:id; this id is linked to the charge.
const { payment, confirmResult } = await confirmPayment(paymentId, {
payment_method: { id: "pm_123" },
payer_identity: { ip_address: clientIp, user_agent: ua },
});
if (payment.status === "REQUIRES_ACTION" && confirmResult.paymentNextAction?.url) {
redirectUser(confirmResult.paymentNextAction.url);
// After user challenge, gateway callback is processed automatically;
// then verify final status with GET payment or webhook.
}See 3DS Simulation for detailed sandbox scenarios.
Charge attempt relationship
One Payment can produce multiple Charges. A new charge is created for each routing candidate; attempt_no reflects routing plan index.
Example: Merchant creates a 1000 TRY Payment. First provider returns soft decline (Charge DECLINED). Routing moves to next candidate; second charge attempt succeeds and Payment becomes SUCCEEDED or AUTHORIZED. Merchant sees one Payment; ops traces attempts via charge list.
Routing stops on success or requires_action; on declined type error it usually stops; on other errors the next candidate may be tried (engine rules depend on candidate pool).
Idempotency and re-confirm
Confirm does not define a mandatory separate Idempotency-Key; network timeout or double-click still risks unnecessary new execution on the same payment.
Best practice:
- After timeout, check existing
statusandchargeswithGET /payments/:idinstead of opening a new payment. - Plan controlled re-confirm when
REQUIRES_PAYMENT_METHODorREQUIRES_ACTION; confirm returns 400 on terminal statuses. - Process webhook events idempotently for duplicate delivery.
Webhook expectations
Frontend or confirm response should not be treated as final accounting record. Payment state changes must be verified via webhook/event system.
Expected approach:
- Confirm response guides user experience (
paymentNextAction, immediatestatus). - Webhook closes merchant backend state (
payment.succeeded,payment.requires_action,payment.authorized, etc.). - Server-side status query is used for reconciliation and incident resolution (
include_charges=truerecommended).
Best practices
- Use Payment and Charge concepts with the same language inside the team.
- Treat Confirm as the execution boundary.
- Accept
REQUIRES_ACTIONas a normal lifecycle part, not an edge case. - Read decline via Charge
DECLINEDandREQUIRES_PAYMENT_METHOD, not Paymentstatusalone. - Decide from
payment.statusand normalized charge fields, not provider raw response. - Test
auto_captureandAUTHORIZEDvsSUCCEEDEDdistinction together with capture/cancel flows. - Test outcomes beyond success in sandbox as well.
Example confirm request
{
"payment_method": {
"id": "pm_123"
},
"routing_strategy": {
"mode": "merchant_direct",
"payment_provider_config_id": "ppc_123"
},
"payer_identity": {
"ip_address": "203.0.113.10",
"user_agent": "Mozilla/5.0 ...",
"ip_address_details": null
}
}Endpoint: POST /v1/payments/:id/confirm. Use API Reference: confirmPayment for the full schema.