Card payin
Charge a bank card: card fields or a saved card token. The payer finishes the payment on the payment page, which runs 3DS when the issuer asks for it. A shop can hold card payins and capture or void them later.
Raw card numbers put your servers in PCI DSS scope. Without certification use a wallet, BLIK or the hosted payment form.
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 payin:
POST /v2/payins/card. Saveidfrom the response. -
Send the payer to
next_action.redirect.url— the payment page. It runs 3DS when the issuer asks for it, then sends the payer toreturn_urls. - If the shop holds card payins, the payin stops at
authorized: callcaptureorvoid. -
Wait for the webhook
payin.completedorpayin.failed. Verify the signature and act ondata.object— the full payin. -
Fallback: if no webhook has come for a minute, you can start polling the status with
GET /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 | POST /v2/payins/card |
createCardPayin |
| Capture a hold | POST /v2/payins/{payin_id}/capture |
capturePayin |
| Void a hold | POST /v2/payins/{payin_id}/void |
voidPayin |
| Refund | POST /v2/payins/{payin_id}/refunds — common for all payins, see Refunds |
createRefund |
| Read one | GET /v2/transactions/{transaction_id} — common for all transactions, see Read one transaction |
retrieveTransaction |
The create, capture and void have their own sections below. Reading returns the Payin object.
POST /v2/payins/card
Creates a payin and returns the link to the payment page.
Request
Headers: Authorization: Bearer <payin shop key>, Content-Type: application/json.
Send either card or card_token.
| 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 |
amount |
object | yes | |
amount.value |
string | yes | Major units, at most two decimals, at least 0.01: "100.50". A string, never a number |
amount.currency |
string | yes | ISO 4217, the shop currency: EUR |
payer |
object | yes | The person who pays |
payer.first_name |
string | with card_token |
With card, if first_name or last_name is omitted, both are taken from card.cardholder_name |
payer.last_name |
string | with card_token |
|
payer.email |
string | yes | Email address |
payer.ip_address |
string | yes | IPv4 or IPv6 of the payer |
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 |
object | one of card, card_token |
|
card.number |
string | yes | 12–19 digits, no spaces, passes the Luhn check |
card.exp_month |
string | yes | 1–12, with or without a leading zero: "12" |
card.exp_year |
string | yes | Two digits: "30" |
card.cvc |
string | yes | 3–4 digits. Never stored |
card.cardholder_name |
string | yes | Latin letters and spaces, as printed on the card |
card_token |
string | one of card, card_token |
card.token of an earlier completed payin of this shop |
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, or at authorized when the shop holds card payins |
return_urls.failure |
string | no | https:// URL. The payment page sends the payer there after failed |
description |
string | no | Up to 255 characters |
curl -X POST https://payments-api.mrcr.io/v2/payins/card \
-H "Authorization: Bearer $PAYIN_SHOP_KEY" \
-H "Content-Type: application/json" \
-d '{
"merchant_reference": "order-10001",
"amount": { "value": "100.50", "currency": "EUR" },
"payer": {
"email": "jane@example.com", "ip_address": "203.0.113.10",
"address": { "country": "US", "state": "CA", "city": "San Francisco", "postal_code": "94105", "line1": "1 Market Street" }
},
"card": { "number": "4111111111111111", "exp_month": "12", "exp_year": "30", "cvc": "123", "cardholder_name": "JANE DOE" },
"return_urls": { "success": "https://shop.example.com/pay/success", "failure": "https://shop.example.com/pay/failure" }
}'
Response 201 Created
The Payin object in data, in requires_action with the link to the payment
page in next_action.redirect.url. A repeat of an earlier call returns the payin in its current
status.
| Header | Meaning |
|---|---|
Idempotent-Replayed: true |
The same merchant_reference and body were sent before. This is the existing payin in its current status; nothing was created again |
{
"data": {
"id": "0b6f4e2a-7c1d-4e8b-9a3f-5d2c8e1b4a70",
"object": "payin",
"merchant_reference": "order-10001",
"method_type": "payin_card",
"status": "requires_action",
"status_details": null,
"amount": { "value": "100.50", "currency": "EUR" },
"refunded_amount": { "value": "0.00", "currency": "EUR" },
"next_action": {
"type": "redirect",
"redirect": { "url": "https://pay-in.mrcr.io/order/0b6f4e2a-7c1d-4e8b-9a3f-5d2c8e1b4a70" },
"expires_at": "2026-09-29T09:10:00Z"
},
"capture_method": "automatic",
"card": { "brand": "visa", "last4": "1111" },
"created_at": "2026-09-29T09:00:00Z",
"updated_at": "2026-09-29T09:00:01Z"
}
}
Errors
No payin is created on any of them.
| HTTP | code |
When | Retry |
|---|---|---|---|
| 400 | validation_failed |
A field is missing or malformed — details[] names it. Also: both card and card_token, or neither; 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 |
Card payins are not enabled for the shop, or the key is of a payout shop | No. Use the payin shop key, or ask Mercuryo to enable the method |
| 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 payin |
| 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 payin may or may not exist. Resend the identical request (R4), never with a new reference.
Payer step: the payment page
The payin is in requires_action with next_action.type: redirect:
-
Send the payer to
next_action.redirect.urlbeforenext_action.expires_at. Open it as a full page or a new window, not in an iframe. - The page runs 3DS when the issuer asks for it.
-
After the final status the page sends the payer to
return_urls.successorreturn_urls.failure; a payin held atauthorizedsends them toreturn_urls.successat the hold. Coming back is not a result — act on the webhook.
How to open the page in a browser or an app — Payer actions.
POST /v2/payins/{payin_id}/capture
Charges a held payin, the full amount. Only from authorized.
Request
Headers: Authorization: Bearer <payin shop key>.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
payin_id |
path | string | yes | id of the payin |
amount |
body | object | no | Partial capture — in the contract, returns 501 not_supported_yet |
curl -X POST https://payments-api.mrcr.io/v2/payins/0b6f4e2a-7c1d-4e8b-9a3f-5d2c8e1b4a70/capture \
-H "Authorization: Bearer $PAYIN_SHOP_KEY"
Response 200 OK
The Payin object in data, in processing. Then the webhook payin.completed,
or payin.capture_failed — the payin is authorized again and you can call capture again.
Safe to repeat: a repeat while the capture is in progress, or after it, returns the payin in its current status.
Errors
| HTTP | code |
When | Retry |
|---|---|---|---|
| 404 | resource_not_found |
No such payin in this shop | No |
| 409 | action_not_allowed |
The payin is not authorized, or it is authorized after a failed void |
No |
| 501 | not_supported_yet |
amount is sent |
No. Capture the full amount |
Also 401, 403, 429, 500 — see Errors.
POST /v2/payins/{payin_id}/void
Releases a held payin. Only from authorized. The payin ends failed with canceled_by_merchant.
Request
Headers: Authorization: Bearer <payin shop key>.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
payin_id |
path | string | yes | id of the payin |
curl -X POST https://payments-api.mrcr.io/v2/payins/0b6f4e2a-7c1d-4e8b-9a3f-5d2c8e1b4a70/void \
-H "Authorization: Bearer $PAYIN_SHOP_KEY"
Response 200 OK
The Payin object in data, in processing. Then the webhook payin.failed with
canceled_by_merchant, or payin.void_failed — the payin is authorized again and you can call
void again.
Safe to repeat: a repeat while the void is in progress, or after it, returns the payin in its current status.
Errors
| HTTP | code |
When | Retry |
|---|---|---|---|
| 404 | resource_not_found |
No such payin in this shop | No |
| 409 | action_not_allowed |
The payin is not authorized, or it is authorized after a failed capture |
No |
Also 401, 403, 429, 500 — see Errors.
The Payin object
Returned by the create, capture, void and GET /v2/transactions/{transaction_id}, and sent in
webhooks as data.object. It has the common fields
of every transaction, with these values and fields for a card payin:
| Field | Type | Always set | Meaning |
|---|---|---|---|
id |
string | yes | UUID. Use it in capture, void, refunds and GET /v2/transactions/{transaction_id} |
object |
string | yes | payin |
method_type |
string | yes | payin_card |
status |
string | yes | created, processing, requires_action, authorized, completed, failed, refunded — see Statuses |
status_details.code |
string | when failed |
See Decline reasons |
refunded_amount |
object | yes | Sum of completed refunds; above zero in refunded |
refunded_amount.value |
string | yes | Major units: "20.00" |
refunded_amount.currency |
string | yes | ISO 4217: EUR |
next_action |
object or null |
yes | Set in requires_action, null in every other status |
next_action.type |
string | in requires_action |
redirect |
next_action.redirect.url |
string | in requires_action |
The payment page |
next_action.expires_at |
date-time | no | When the provider stops waiting for the payer; after it the payin ends failed |
capture_method |
string | yes | automatic — charged at once. manual — the shop holds card payins at authorized |
card |
object | no | The card |
card.brand |
string | no | Card scheme: visa, mastercard |
card.last4 |
string | no | Last four digits |
card.token |
string | no | Set on a completed payin created with save_card: true. Pass it as card_token |
Statuses
| Status | What it means | The money | Webhook | What you do | Next |
|---|---|---|---|---|---|
created |
Accepted and recorded | — | — | Wait | requires_action, failed |
requires_action |
Waiting for the payer on the payment page | — | — | Send the payer to next_action.redirect.url |
processing, failed |
processing |
Checks run; the payin, a capture or a void is at the provider | — | — | Wait | authorized, completed, failed |
authorized |
Held on the card. Also after a failed capture or void | Held | payin.authorized, payin.capture_failed, payin.void_failed |
Call capture or void |
processing |
completed |
Final. Charged | Charged | payin.completed |
Fulfil the order | refunded |
refunded |
Final. Charged, then refunded in part or in full. refunded_amount says how much |
Refunded in part or in full | payin.refunded on every completed refund |
Reflect the refund in your order | — |
failed |
Final. Not charged, or the hold is released. status_details.code says why |
Released | payin.failed |
Act on the decline reason | — |
-
Only
completed,failedandrefundedare outcomes. A final status never changes, with one exception: a completed refund moves the payin fromcompletedtorefunded. requires_actioncomes in the create response, so it needs no webhook.- An unknown status is not final — keep waiting (R13).
Decline reasons
status_details.code on a failed card payin. Every decline is final: a new attempt is a new payin
with a new merchant_reference.
| Code | When | What you do |
|---|---|---|
card_not_supported |
The card's BIN is unknown or not accepted | Ask the payer for another card |
method_unavailable |
No route for this card right now | Try again later with a new payin |
risk_rejected |
Rejected by antifraud | Do not retry automatically |
provider_declined |
The provider or the issuer declined, including a failed 3DS | Ask the payer to try again or use another card |
canceled_by_merchant |
You voided the hold | — |
internal_error |
Failure on our side; nothing was charged | Try again with a new payin. Quote id to support if it repeats |
An unknown code: handle it like provider_declined.
Webhooks
type |
When | data.object |
|---|---|---|
payin.authorized |
The payin is held: authorized |
The Payin object |
payin.capture_failed |
The provider rejected a capture; the payin is authorized again |
The Payin object |
payin.void_failed |
The provider rejected a void; the payin is authorized again |
The Payin object |
payin.completed |
The payin reaches completed |
The Payin object |
payin.refunded |
A refund of the payin completes — the first one moves it to refunded |
The Payin object, with refunded_amount |
payin.failed |
The payin reaches failed |
The Payin object, with status_details |
refund.completed, refund.failed |
A refund of this payin reaches a final status | The Refund object — see Refunds |
Headers, signature check and redelivery — Webhooks.
In the sandbox
The cents of amount.value select the outcome.
| Cents | Example | Outcome |
|---|---|---|
| any other | 100.00 |
completed; authorized when the shop holds card payins |
.51 |
100.51 |
failed — provider_declined |
.52 |
100.52 |
failed — risk_rejected |
.54 |
100.54 |
failed — card_not_supported |
.55 |
100.55 |
failed — method_unavailable |
.56 |
100.56 |
failed — internal_error |
.61 |
100.61 |
processing for 60 seconds, then completed |
Sandbox keys and fixtures — Sandbox.
Next: Apple Pay