Skip to content

Card Handoff

POST /v1/payments is cardless: it only creates a payment request and returns a paymentRef (a single-use payment reference) and a handoffUrl. To put the card into the bank’s 3D Secure relay, you redirect the buyer’s browser/WebView to this handoffUrl together with the card.

The handoffUrl is Paytalya’s separate handoff page (the pay.* origin): it does not collect the card for you, but takes the card you collected plus the paymentRef, builds the bank’s 3D Secure form itself, and runs the bank’s 3D relay. The card arrives at this page via a form POST from the buyer’s browser. There is no hosted link or payUrl you just send; the page is reached with the card + paymentRef.

FieldValue
TargetThe handoffUrl from the POST /v1/payments response (pay.* origin, Paytalya handoff page).
MethodPOST (navigating form submission).
BodypaymentRef + the card fields you collected (pan, expireMonth, expireYear, cv2).

Fields submitted:

FieldDescription
paymentRefThe single-use payment reference from the POST /v1/payments response. Amount/terminal/returnUrl are bound to it; the browser cannot change them.
panCard number: the value you collected on your own checkout.
expireMonth / expireYearExpiry month/year (MM / YY).
cv2Card security code.

Amount, terminal, and returnUrl are not sent to the handoff; they are fixed on Paytalya’s side, bound to the paymentRef. The handoff carries only the card + paymentRef.

The buyer’s browser POSTs to the handoffUrl via a form. In the example below the card is masked (the real value comes from the buyer’s browser memory); never write a real card number into docs or logs:

<form id="paytalya-handoff" method="POST" action="https://pay.paytalya.example/checkout">
<input type="hidden" name="paymentRef" value="pr_3Kd9xQ1w2E" />
<input type="hidden" name="pan" value="4242 42** **** 4242" />
<input type="hidden" name="expireMonth" value="12" />
<input type="hidden" name="expireYear" value="28" />
<input type="hidden" name="cv2" value="***" />
</form>
<script>document.getElementById('paytalya-handoff').submit();</script>

After the submission, everything runs on the handoff page: the page starts the bank’s 3D relay, the buyer completes 3D Secure, the callback is processed, and in the end the buyer’s browser returns to your returnUrl via 303. You do not build the bank’s 3D form; the handoff page produces the auto-submit form.

On mobile the flow is the same; the only difference is that you create the browser context with a WebView:

  1. Collect the card on your app’s native screen.
  2. Open a WebView and POST the card + paymentRef to the handoffUrl:
    • Android: WebView.postUrl(handoffUrl, body); the body is application/x-www-form-urlencoded paymentRef + card fields.
    • iOS: load the WebView with a URLRequest whose httpMethod = "POST" (targeting the pay.* surface).
  3. 3D Secure completes inside the WebView; in the end the WebView lands on your returnUrl.

A paymentRef is single-use and 3D can be started only once:

  • If the same paymentRef is used for a second handoff, the request is rejected (payment_prepare_already_started).
  • If the paymentRef is unknown/invalid or expired, the handoff is rejected (payment_ref_invalid / payment_ref_expired); in that case the card is not processed.

If the buyer abandons or fails 3D, do not reuse the same paymentRef; the related payment becomes failed/expired. To retry, create a new payment request (POST /v1/payments) and start the handoff again with the new paymentRef/handoffUrl.