Accept a one-time payment
The complete server-side flow for MintCash integration. No PCI scope on your side.
This is the simplest way to accept a card payment. Your code never touches the card — the customer enters it on the provider's hosted page. PCI scope stays out of your stack.
The flow
Implementation
Create the payment
Send POST /payments from your server. Include the customer's identity (so the same customer is reused across orders) and a returnUrl so the provider knows where to send the browser after.
const r = await fetch("https://sandbox.mintcash.me/payments", {
method: "POST",
headers: {
Authorization: `Basic ${btoa(`${publicKey}:${secretKey}`)}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
externalId: orderId, // your unique order ID — used for idempotency
amount: 49.99, // major units — dollars, not cents
currency: "USD",
returnUrl: `${baseUrl}/order/${orderId}/complete`,
customer: {
externalId: customerId,
email: customer.email,
name: customer.name,
},
}),
});
const { payment, redirectUrl } = await r.json();The response includes the payment record (status created) and a redirectUrl — the URL of the hosted page on the provider.
Redirect the customer
Send the browser to redirectUrl. From your server-rendered checkout page, this is a 302. From a SPA, it's a window.location.assign(redirectUrl).
return Response.redirect(redirectUrl, 302);Handle the completion
After the customer finishes, they either land back on your returnUrl or see a completion screen on the payment page, depending on how your account's payment page is configured — confirm the exact behavior with your account manager. Either way, do not fulfil yet. The customer's browser being done tells you nothing about the money; the webhook tells you the money moved.
On your completion page, show a "thanks, we're processing your order" state. Look up the payment by externalId and poll or wait for the webhook:
// GET /order/[orderId]/complete
const payment = await fetch(
`https://sandbox.mintcash.me/payments/${stored.paymentId}`,
{ headers: { Authorization: ... } }
).then((r) => r.json());
if (payment.status === "succeeded") {
return renderThankYou();
} else if (payment.status === "failed") {
return renderFailure(payment.failureMessage);
} else {
return renderProcessing(); // pending — webhook is on its way
}Fulfil on the webhook
When payment.succeeded arrives, that's your trigger to ship the order, grant the credits, send the email.
// POST /webhooks/mintcash
import { verifyWebhook } from "@/lib/mintcash";
export async function POST(req: Request) {
const body = await req.text();
const signature = req.headers.get("x-signature");
if (!verifyWebhook(body, signature, process.env.MINTCASH_SIGNING_SECRET)) {
return new Response("invalid signature", { status: 401 });
}
const event = JSON.parse(body);
if (event.environment !== process.env.MINTCASH_ENV) {
return new Response("wrong env", { status: 400 });
}
// Dedupe by eventId
if (await alreadyProcessed(event.eventId)) {
return new Response("ok", { status: 200 });
}
if (event.event === "payment.succeeded") {
await fulfillOrder(event.data.externalId);
}
await markProcessed(event.eventId);
return new Response("ok", { status: 200 });
}See signature verification for the full reference implementation.
Embed the payment form
You don't have to send customers away to the full hosted page — the same page can render inside an iframe on your own site, signalling completion back to your page via postMessage. See Embed the checkout for the iframe setup, origin registration, and the full message reference.
Customize the payment page
The page you get is MintCash's default look. For theming the colors and fonts, or if you want to request a completely custom checkout built from your design, see Customize the payment page.
What to test
Exercise these scenarios in sandbox using the cards from the test cards reference:
- Happy path with the success card — should hit
payment.succeeded - Failure with a failing card — should hit
payment.failed - 3DS challenge with the 3DS-enrolled success card — customer sees the challenge, then bounces back
- Duplicate
externalId— second request returns the same payment record - Tampered webhook — your endpoint returns 401 and doesn't fulfil
Subscriptions use the same shape
When you need recurring billing, see Set up a subscription — the API and webhook shape are the same; you also pick a billing interval and we handle renewals.