Hosted payment form
Take a card payment on the payment form: you create a pay session, the payer enters the card on the form. Card data never passes through your servers, so your servers stay out of PCI DSS scope.
When the payer submits the form, a card payin with the session's
merchant_reference is created. From then on it is a regular card payin: the same statuses, hold,
refunds and webhooks. One payin per session.
Use the key of a payin shop; a payout shop key gets 403 method_not_enabled. Rules shared by every
method — Authentication, Idempotency,
Errors, Webhooks — apply here and are
not repeated.
Flow
- Generate
merchant_referenceand save it with your order before the call. -
Create the session:
POST /v2/pay-sessions. Send the payer tourl, or embed it in an iframe on a domain allowed for the shop. -
The payer enters the card and submits. A card payin is created; the payment page runs 3DS when
the issuer asks for it, then sends the payer to
return_urls. -
Wait for the webhook
payin.completedorpayin.failed, orpayin.authorizedif the shop holds card payins. Verify the signature and act ondata.object— the full payin. -
Fallback: if no webhook has come for a minute, you can start polling: find the payin with
GET /v2/transactions?merchant_reference=…while you do not know its id, then read it withGET /v2/transactions/{transaction_id}until it is final. Polling instead of webhooks loads the API and gets429 rate_limited(R20).
Endpoints
| Step | Endpoint | Operation |
|---|---|---|
| Create a session | POST /v2/pay-sessions |
createPaySession |
| Capture, void, refund | As for a card payin | capturePayin, voidPayin, createRefund |
| Read the payin | GET /v2/transactions?merchant_reference=… or GET /v2/transactions/{transaction_id} — common for all transactions, see Transactions |
listTransactions, retrieveTransaction |
The create has its own section below. It returns the Pay session object; the payin is the card Payin object.
POST /v2/pay-sessions
Creates a pay session and returns the link to the form.
Request
Headers: Authorization: Bearer <payin shop key>, Content-Type: application/json.
| Field | Type | Required | Format and limits |
|---|---|---|---|
merchant_reference |
string | yes | 1–100 characters. Unique per merchant across payins, refunds, payouts and pay sessions. The idempotency key of this call; the payin gets the same value |
amount |
object | yes | |
amount.value |
string | yes | Major units, at most two decimals, at least 0.01: "75.00". A string, never a number |
amount.currency |
string | yes | ISO 4217, the shop currency: EUR |
description |
string | yes | Up to 255 characters. Shown to the payer on the form |
expires_at |
date-time | yes | When the form stops accepting the payment. In the future, with a time zone offset: 2026-09-29T10:40:00Z |
payer |
object | yes | The person who pays |
payer.email |
string | yes | Email address |
payer.first_name |
string | no | If first_name or last_name is omitted, both are taken from the cardholder name the payer types on the form |
payer.last_name |
string | no | |
payer.ip_address |
string | no | IPv4 or IPv6. If omitted, the IP the payer submits the form from |
payer.address |
object | yes | Billing address |
payer.address.country |
string | yes | ISO 3166-1 alpha-2: US |
payer.address.state |
string | for US, CA, AU payers | ISO 3166-2 subdivision code without the country prefix, 2–3 letters: CA |
payer.address.city |
string | yes | Not digits only |
payer.address.postal_code |
string | yes | |
payer.address.line1 |
string | yes | Street and house number |
payer.address.line2 |
string | no | Apartment, suite, floor |
card_token |
string | no | card.token of an earlier completed payin of this shop. The form shows the saved card instead of the card fields |
save_card |
boolean | no | true — the payin returns card.token when it is completed. Works if the shop allows saving cards |
return_urls |
object | no | |
return_urls.success |
string | no | https:// URL. The payment page sends the payer there after completed |
return_urls.failure |
string | no | https:// URL. The payment page sends the payer there after failed |
curl -X POST https://payments-api.mrcr.io/v2/pay-sessions \
-H "Authorization: Bearer $PAYIN_SHOP_KEY" \
-H "Content-Type: application/json" \
-d '{
"merchant_reference": "order-50001",
"amount": { "value": "75.00", "currency": "EUR" },
"description": "Order 50001",
"expires_at": "2026-09-29T10:40:00Z",
"payer": {
"email": "jane@example.com",
"address": { "country": "US", "state": "CA", "city": "San Francisco", "postal_code": "94105", "line1": "1 Market Street" }
},
"return_urls": { "success": "https://shop.example.com/pay/success", "failure": "https://shop.example.com/pay/failure" }
}'
Response 201 Created
The Pay session object in data. A repeat with the same
merchant_reference and body returns the same session and the same url, also after expires_at.
| Header | Meaning |
|---|---|
Idempotent-Replayed: true |
The same merchant_reference and body were sent before. This is the existing session; nothing was created again |
{
"data": {
"id": "9c1e7a2f4b8d3c6e0a5f1b9d7e3c2a8f4b6d0e1c9a7f3b5d2e8c4a6f0b1d3e5c",
"object": "pay_session",
"merchant_reference": "order-50001",
"amount": { "value": "75.00", "currency": "EUR" },
"url": "https://pay-in.mrcr.io/pay/9c1e7a2f4b8d3c6e0a5f1b9d7e3c2a8f4b6d0e1c9a7f3b5d2e8c4a6f0b1d3e5c",
"expires_at": "2026-09-29T10:40:00Z",
"payin_id": null,
"created_at": "2026-09-29T09:40:00Z"
}
}
Errors
No session is created on any of them.
| HTTP | code |
When | Retry |
|---|---|---|---|
| 400 | validation_failed |
A field is missing or malformed — details[] names it. Also: expires_at is not in the future; an unknown card_token; the amount has more than two decimals or is not in the shop currency |
No. Fix the request |
| 401 | authentication_failed |
The key is missing, malformed, revoked, or sent to the wrong host | No |
| 403 | method_not_enabled |
The payment form is not set up for the shop, or the key is of a payout shop | No. Use the payin shop key, or ask Mercuryo to set it up |
| 409 | merchant_reference_conflict |
The reference was used with a different body | No. Resend the original body, or use a new reference for a new session |
| 429 | rate_limited |
Too many requests | Yes, after Retry-After seconds |
| 500 | internal_error |
Failure on our side | Yes, the identical request |
After a timeout or a dropped connection the session may or may not exist. Resend the identical request (R4), never with a new reference.
Opening the form
- Full page or a new window. Send the payer to
url. -
Iframe. Put
urlin an iframe on a domain allowed for the shop. The list of domains is set for the shop by Mercuryo; on any other domain the browser refuses to show the form. - The look of the form — theme, terms and conditions — is set for the shop.
-
After the payer submits, the form goes on to the payment page: it runs 3DS when the issuer asks for
it and then sends the payer to
return_urls. Coming back is not a result — act on the webhook.
The Pay session object
Returned by the create.
| Field | Type | Always set | Meaning |
|---|---|---|---|
id |
string | yes | Session id; the last part of url |
object |
string | yes | pay_session |
merchant_reference |
string | yes | As you sent it; the payin gets the same value |
amount |
object | yes | As you sent it |
amount.value |
string | yes | Major units: "75.00" |
amount.currency |
string | yes | ISO 4217: EUR |
url |
string | yes | The payment form |
expires_at |
date-time | yes | UTC. When the form stops accepting the payment |
payin_id |
string or null |
yes | The payin created when the payer submitted the form; null until then |
created_at |
date-time | yes | UTC |
After the form
The result of the payment is the status of the payin — see card payin statuses.
payin_id |
expires_at |
What it means | What you do |
|---|---|---|---|
null |
in the future | The payer has not submitted the form yet | Wait |
null |
passed | The session expired unpaid | Create a new session with a new merchant_reference |
| set | — | The payer submitted the form | Follow the payin by its webhooks |
A declined payer cannot try another card on the same session: a new attempt is a new session.
Webhooks
The session sends no webhooks of its own. The payin created from it sends the
card payin webhooks: payin.authorized, payin.capture_failed,
payin.void_failed, payin.completed, payin.failed, and refund.* for its refunds. Match them to
your order by merchant_reference.
Headers, signature check and redelivery — Webhooks.
In the sandbox
The form takes the card test data. The cents of amount.value select the
outcome of the payin, as for a card payin.
Next: Card payout