The API defines bearer authentication in the Authorization header. Use HTTPS and keep the token out of browser code and application logs.
Sign charge requests
For POST /v2/Charge, add your server IP under Management console Security ALLOWED IPs. Calculate HMAC-SHA256 over the exact JSON request body using the HMAC CHARGE KEY, and send the lowercase hexadecimal digest in the HMAC header.
PHP
$body = json_encode([
'PaymentToken' => 'YOUR_PAYMENT_TOKEN',
'Amount' => 55.55,
'Currency' => 'USD'
]);
$signature = hash_hmac('sha256', $body, getenv('MYPAY_HMAC_CHARGE_KEY'));
// Send this exact $body with the HMAC: $signature header.
Replace example values and credentials before use.
Create a payment from your server, send the shopper to the MyPay checkout page and receive the signed result on your webhook.
Quick start
Base URL
The base URL is configured per environment. In the WooCommerce and OpenCart plugins it is the Service URL setting. The checkout service also uses SERVICE_DOMAIN to build redirect URLs.
Use the following format when calling the service:
HTTPS
{SERVICE_URL}/api
Prerequisites
MyPay issues you three values. All three are shown once, at the moment they are issued, and are never readable again — copy them then.
Value
Looks like
Used for
Checkout API key
mpc_…
Creating payments. This is the key that lives in your shop plugin.
Full access API key
mpf_…
Creating payments, and refunds and cancellations. Optional — issued on request.
Webhook signing secret
whsec_…
Verifying that a webhook really came from MyPay.
You also need your subsystem ID, and a webhook URL on your server that can receive payment results.
Authentication
Every /webhooks endpoint requires two things:
Your API key as a Bearer token.
Your subsystem ID in the request body.
HTTP
POST /api/webhooks/transaction
Content-Type: application/json
Authorization: Bearer mpc_YOUR_CHECKOUT_KEY
A request without a key gets 401. A request without subsystemId gets 400. This applies to refund and cancel too, even though they carry no other body fields.
Webhooks we send you are not authenticated with your API key. They are signed — see Webhook. Nothing MyPay sends you ever contains your key, and you should never accept a webhook on the basis of a bearer token.
API keys and what they can do
There are two roles, and the key tells you which one it is by its prefix:
Prefix
Role
POST /transaction
POST /refund/…
POST /cancel/…
mpc_
Checkout
✓
✕403
✕403
mpf_
Full access
✓
✓
✓
The refusal is:
JSON
{ "success": false, "message": "This API key is not permitted to perform this action." }
Use the checkout key everywhere you can. It is the key that ends up in a plugin's settings, in a .env file, in version control, and in the hands of every developer the shop has ever hired. A leaked checkout key lets someone create payments; a leaked full access key lets them send your money back out.
You can tell a key's role without calling us. The role is part of the key and is covered by the hash we store, so mpf_… can never resolve as a checkout key or the reverse. Read the prefix — that is exactly what our own plugins do to decide whether to offer a refund button.
Both keys can be rotated from your MyPay account. After a rotation the previous key keeps working for 7 days, so you can update your servers without downtime. A webhook secret's grace period is 24 hours.
merchantOrderId identifies the payment: sending the same one twice returns the existing payment rather than creating a second one, so a retried request is safe.
That only holds while the price agrees. If a payment already exists for that order number with a different amount or currency, the request is refused:
JSON
{ "success": false, "message": "Order 'ORDER-123456' already has a payment for a different amount." }
Read that as: something already opened a payment for this order at another price. Investigate it — do not retry with a new order number.
2. Cancel transaction
POST/webhooks/cancel/{merchantOrderId}
Key required: full access (mpf_)
Void a paid transaction and return the full amount immediately.
JSON
{ "subsystemId": "YOUR_SUBSYSTEM_ID" }
Replace example values and credentials before use.
Unique per event and identical across retries. Store it and ignore an event you have already handled.
type
payment.succeeded or payment.failed. Treat any unknown type as "ignore", not "fail".
data.status
success or failure. Anything that is not exactly success means no money was taken.
data.amount
A string, always. Compare in cents — "500" and "500.00" are the same amount.
Verifying the signature
The header is t=<unix seconds>,v1=<hex>. Compute HMAC-SHA256(webhook_secret, "<t>.<raw body>") and compare it to v1.
Four rules, each of which matters:
Sign the raw bytes you received. Re-serialising the JSON produces different bytes and the signature will not match.
Reject anything older than about 5 minutes.t is inside the signed string, so it cannot be edited — which is the only reason this check stops a captured webhook being replayed tomorrow.
Accept any matching v1. During a secret rotation the header carries two of them, one per secret, so a half-updated fleet keeps working.
Compare in constant time.
JavaScript
const crypto = require('node:crypto');
function verify(header, rawBody, secret, toleranceSeconds = 300) {
const parts = Object.create(null);
const signatures = [];
for (const part of String(header).split(',')) {
const [k, v] = part.trim().split('=', 2);
if (k === 't') parts.t = v;
else if (k === 'v1') signatures.push(v);
}
if (!parts.t || !/^\d+$/.test(parts.t) || signatures.length === 0) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t)) > toleranceSeconds) return false;
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
return signatures.some(
(s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)),
);
}
A PHP reference implementation lives in both shop plugins: Mypay\Client::verifySignature() in the OpenCart plugin, DC_Mypay_Webhook::verify() in the WooCommerce one.
Retries
If your endpoint does not answer 2xx, MyPay retries after 10s, 60s, 5min and 15min — five attempts over about twenty minutes. Every attempt carries the same event_id and a fresh t, so the signature differs each time and must be verified each time.
Answer 200 as soon as you have stored the event. Do the slow work afterwards; a handler that ships the order before replying will be retried, and will ship it twice.
Checking the amount
MyPay cannot do this for you. We know what the payment was created for, not what the order is worth — only your shop holds that. Before marking an order paid, confirm that data.amount and data.currency match the order. A payment opened for one cent against a five hundred euro order looks perfectly consistent to us.
Examples
All MyPay API calls must be made from your backend server, never from the frontend. The frontend never sees an API key.
app.post('/create-payment', async (req, res) => {
const { orderId } = req.body;
// Get order details from your database — never from the browser.
const order = await getOrderFromDatabase(orderId);
const response = await fetch(`${SERVICE_URL}/api/webhooks/transaction`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// The checkout key. This server does not need to be able to refund.
'Authorization': `Bearer ${process.env.MYPAY_CHECKOUT_KEY}`
},
body: JSON.stringify({
subsystemId: process.env.MYPAY_SUBSYSTEM_ID,
amount: order.totalAmount,
currency: 'EUR',
merchantOrderId: orderId,
successUrl: 'https://yoursite.com/success',
cancelUrl: 'https://yoursite.com/cancel',
webhookUrl: 'https://yoursite.com/webhook',
customerEmail: order.customerEmail,
orderItems: order.items
})
});
const result = await response.json();
if (response.ok && result.success) {
res.json({ redirectUrl: result.redirectUrl });
} else {
// result.message says why. A 409 means this order number already has a
// payment for a different amount — look into it, do not retry.
res.status(400).json({ error: result.message });
}
});
Replace example values and credentials before use.
Backend — webhook handler
JavaScript
// The raw body is required: verification is over bytes, not over parsed JSON.
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const raw = req.body.toString('utf8');
if (!verify(req.headers['mypay-signature'], raw, process.env.MYPAY_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(raw);
if (await alreadyHandled(event.event_id)) return res.sendStatus(200);
if (event.type === 'payment.succeeded') {
const order = await getOrderFromDatabase(event.data.merchantOrderId);
// Only your shop can make this check.
const sameAmount =
Math.round(Number(event.data.amount) * 100) === Math.round(order.totalAmount * 100) &&
event.data.currency === order.currency;
if (sameAmount) await markPaid(order.id);
}
await recordHandled(event.event_id);
res.sendStatus(200); // Fulfil afterwards, not before answering.
});
Backend — refund
Needs a full access key. If all you hold is a checkout key, refund from your MyPay account instead — the call below will be refused with 403.
Replace example values and credentials before use.
Shop plugins
The MyPay plugins for WooCommerce and OpenCart do all of the above for you. Two things worth knowing:
They read the key's role from its prefix and adapt. With a mpf_ key the order page refunds through MyPay. With a mpc_ key it does not call us at all: the refund is recorded against the order in your shop, the plugin says so plainly, and you return the money from your MyPay account. No failed call, no error, no order that claims a refund which never happened.
They verify the webhook signature, and ignore anything that does not verify. If you never paste a webhook secret into the plugin settings, no order will ever be marked paid.
Pass a payment token to your server instead of raw card details.
Set your public key
Mypay.js exchanges customer and card details for a temporary token. Set the public key from Management console Security.
JavaScript
Mypay.SetPublicKey('YOUR_PUBLIC_KEY');
Mypay.CreateToken().done(function (token) {
// Send the token to your server to continue the payment.
}).fail(function (error) {
// Show a helpful message and let the customer try again.
});
Replace example values and credentials before use.
Map your form fields
The library reads inputs with data-mypay attributes. The table lists the fields in the published tokenization guide.
Field
Type
Details
emailRequired
string
Customer email
firstNameRequired
string
First name
lastNameRequired
string
Last name
billingAddressRequired
string
Billing address
zipRequired
string
Billing postal code
numberRequired
string
Card number
monthRequired
int
Expiry month
yearRequired
int
Expiry year
cvvRequired
int
Card security code
subdomain
string
Project subdomain
amount
decimal
May be needed for 3D Secure
currency
string
May be needed for 3D Secure
vatCountry
string
VAT country code
vatNumber
string
VAT number
paymentMethodID
long?
Preselect an available payment method
Use the token on your server
The existing guide gives temporary tokens a 30-minute lifetime. Complete the server-side API step before expiry. For subsequent charges, retrieve the permanent customer payment token through the charge or customer response.
A custom card form affects your PCI DSS scope. Confirm the requirements for your exact implementation before collecting card data.
Build your own checkout around MyPay customers, products, charges and invoices.
Build the payment flow
A full API integration uses your own checkout experience. Start with a company, project and gateway, then create the customer and payment method needed for the charge.
These fields are marked required in CustomerDTO. Use the project subdomain configured for your company. The full reference includes shipping details, business information and payment methods.
Replace example values and credentials before use.
Connect the charge to your order
ChargeDTO supports MerchantUniqueID for your transaction reference or CustomFields for multiple metadata values. Use one or the other, as specified by the API. Sign the request body using the charge HMAC key.
The v2 contract defines TransactionNotification for charge and payment updates. It includes the transaction ID, status, amount, customer data, signature and webhook type.
Verify incoming notifications before updating an order. Your implementation should account for signature validation, duplicate delivery and status reconciliation. The final verification instructions will be added here.
Match the notification to the stored order, check amount and currency, and ensure that processing the same event twice cannot fulfil the order twice.