ROKIConnect
Connectors Start API SDKs Migrate MCP Portal ES

Void, refund and receipts

13.0 There is no authorize-then-capture, and where that limit comes from

Connect charges in one step. A payment authorizes and captures together - a sale - and there is no endpoint that places a hold now and takes the money later.

If you ship goods, this is the constraint to design around: you cannot reserve funds at checkout and capture when the box leaves the warehouse. What you have instead is reversal after the fact - void while the transaction is unsettled, refund once it is not, both in this section.

Whose limit it is, answered by the API team on 2026-08-25, because it changes how you plan:

Does the processor support it? Yes. PowerTranz has authorize and capture as separate calls
Does the acquiring bank block it? No
So why is it missing? Connect does not expose it. It is an API/product gap, not a technical one

That is worth knowing rather than assuming. A limit that comes from the processor is permanent and you build around it forever; a limit that is "not exposed yet" is a roadmap item worth asking for when you have a merchant who needs it.

13.0b 3-D Secure: version, and who carries the fraud loss

Connect authenticates with 3DS2 (EMV 3-D Secure 2.x); the processor reports the exact sub-version, e.g. 2.1.0.

When authentication succeeds, fraud liability can move from the merchant to the card issuer. There is no API call for it and no switch to turn on: it follows automatically from the card network's own rules, when the ECI value, the authentication status and the proof of authentication (the CAVV) line up.

Network Fully authenticated Attempted Failed - no shift
Visa / Amex ECI 05, status Y, CAVV present ECI 06, status A, CAVV present ECI 07, status N / U / R
Mastercard ECI 02, status Y, CAVV present ECI 01, status A, CAVV present ECI 00, status N / U / R

Two caveats, both stated by the API team:

None of this is visible on the Connect payment object. The processor returns the ECI, the CAVV, the authentication status and the protocol version, but Connect does not surface any of them, and there is no liability_shift flag. So you cannot branch on it, and you cannot show a merchant whether a given payment shifted. Treat authentication as something that happened, not as something you can read back.

These act on a transaction, identified by the transaction_id UUID from the payment response - never by the numeric payment id.

That distinction matters because a reusable link can carry several transactions: voiding or refunding one customer's transaction leaves the others untouched.

Operation Path
Void POST /payments/{transaction_id}/void
Refund POST /payments/{transaction_id}/refund
Receipt metadata GET /payments/{transaction_id}/receipt
Receipt PDF GET /payments/{transaction_id}/receipt/download

13.1 Passing the wrong identifier

These routes want the transaction UUID. Hand them the numeric payment id and the endpoint answers 404 with code: "transaction_id_expected", naming the identifier it wanted:

POST /payments/9e2b6a34-6f1d-4e2a-8c9b-2f7a1d4e5c6b/void  ->  the route matched
POST /payments/873/void                                    ->  404 {"code":"transaction_id_expected",
                                                                  "message":"This endpoint requires
                                                                  transaction_id (the UUID from the
                                                                  payment or charge response), not
                                                                  the payment numeric id."}

That is a live endpoint refusing the identifier, not a missing route. The distinction matters because the two have opposite fixes: not_found means change the URL, transaction_id_expected means change the identifier.

Changed 2026-08-21. This case used to return the routing 404 ("The route ... could not be found."), and earlier versions of this guide - and the sample clients in it - told you to detect it that way. If your integration still branches on "could not be found", it now misreads this error as "payment does not exist". Branch on code. The message here is localized; the routing 404 prose is not.

13.2 Void or refund?

Void Refund
When Same day, before settlement After settlement
Amount Full only Full or partial
Speed Immediate 3-5 business days back to the card
Customer sees Never actually charged Money returned
Webhook payment.voided payment.refunded / payment.partially_refunded
Typical case Mistake, immediate cancel Return, dispute, overcharge

Eligibility is governed by the transaction's configured time window, not only by settlement state. Attempting a refund too early can return 422 with a processor message such as "Invalid transaction" - that text comes from the processor and is not a response ROKI guarantees.

Implement reversal as: try void first; if it is rejected as not voidable or already settled, refund instead.

13.3 Void

POST /api/connect/v1/payments/{transaction_id}/void
{ "reason": "Customer cancelled" }        # optional, max 2000 chars

Returns the payment with status: "voided". Rejections arrive as 422 under errors.void: already voided, already refunded (use refund), not in a voidable state, or already settled.

A void also sets the refund fields. After voiding, the payment carries

{ "status": "voided", "refunded_amount": 25, "refund_status": "full" }

even though nothing was refunded. So refund_status === 'full' does not mean "this was refunded" - it is equally true of a void. Branch on status (voided vs refunded vs partially_refunded) and treat the refund fields as the amount returned to the cardholder by any means, not as evidence of which operation was used. Reporting that counts refunds by refund_status will silently count voids as refunds, and the two are commercially different: a void never reaches the customer's statement, a refund does.

The payment.voided webhook arrived about a second later on both transactions, carrying the same fields.

13.4 Refund

POST /api/connect/v1/payments/{transaction_id}/refund
{ "amount": 500.00, "reason": "Partial return" }

amount is required, minimum 0.01, up to the remaining refundable amount. A partial refund leaves partially_refunded; a full one sets refunded. 422 under errors.amount (below minimum, above remaining) or errors.refund (voided transaction, not refundable).

Refund works the same for a transaction created by any mode, including an embedded checkout or a saved-card charge. There is no separate refund endpoint for token charges: use the id the charge returned as the transaction_id here.

13.5 Receipts

GET /payments/{transaction_id}/receipt returns payment_id, transaction_id and a public receipt_url. Add /download for the PDF. Receipts survive a void or refund as long as the original charge still has a transaction.

13.6 A note on identifiers

Two different handles, and mixing them is the most common mistake:

Both are worth persisting when a payment is charged. Listing (9.2) can recover a lost id, but only if you know what you are looking for.