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:
- The numeric payment
idis forGET /payments/{id}. - The
transaction_idUUID is for void, refund and receipts.
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.
