ROKIConnect

Void, refund and receipts

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 looks like a missing endpoint

The route only matches the UUID format, so a numeric payment id produces a routing 404:

POST /payments/9e2b6a34-6f1d-4e2a-8c9b-2f7a1d4e5c6b/void  ->  the route matched
POST /payments/873/void                                    ->  "The route ... could not be found."

If you see that message on void, refund or a receipt, you passed the payment id where the transaction UUID belongs. The endpoint is there.

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.