Accept a one-time payment (HPP)
The complete server-side flow for a hosted payment page 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: 4999, // in 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 return
The provider redirects back to your returnUrl after the customer finishes. Do not fulfil yet. The redirect tells you the customer's browser is done; the webhook tells you the money moved.
On the return 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.
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.