Your First Payment
Step 2 of 4. Create a checkout session and redirect your customer to pay.
In this guide you will create a payment using the Hosted Payment Page (HPP). Your server creates a session, redirects the customer to a Miracle-hosted checkout page, and receives a webhook when the payment completes. No PCI paperwork beyond SAQ-A.
Prerequisites
Before you start, make sure you have:
- A Miracle sandbox account
- Your test API key (
sk_test_...) — see Step 1: Get API Credentials
How HPP works
- Your server creates a Checkout Session via the API.
- You redirect the customer to the checkout URL returned in the response.
- The customer completes payment on Miracle's hosted page.
- Miracle sends a webhook to your server with the result and redirects the customer back.
Step 1: Create a Checkout Session
Call POST /v1/hpp/sessions with the order details. Miracle returns a session with a url you will redirect to.
curl -X POST https://api.miracle.com/v1/hpp/sessions \
-H "Authorization: Bearer sk_test_your_secret_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1234" \
-d '{
"amount": {
"currency": "USD",
"valueMinor": 5000
},
"lineItems": [
{
"label": "Blue T-Shirt",
"unitAmount": { "currency": "USD", "valueMinor": 2500 },
"quantity": 2
}
],
"merchantReference": "order-1234",
"successUrl": "https://your-site.com/payment/success",
"cancelUrl": "https://your-site.com/payment/cancel"
}'const response = await fetch('https://api.miracle.com/v1/hpp/sessions', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_test_your_secret_key',
'Content-Type': 'application/json',
'Idempotency-Key': 'order-1234',
},
body: JSON.stringify({
amount: {
currency: 'USD',
valueMinor: 5000,
},
lineItems: [
{
label: 'Blue T-Shirt',
unitAmount: { currency: 'USD', valueMinor: 2500 },
quantity: 2,
},
],
merchantReference: 'order-1234',
successUrl: 'https://your-site.com/payment/success',
cancelUrl: 'https://your-site.com/payment/cancel',
}),
});
const session = await response.json();The response contains the session details and the checkout URL:
{
"data": {
"id": "cs_test_abc123",
"status": "open",
"url": "https://checkout.miracle.com/cs_test_abc123",
"amount": {
"currency": "USD",
"valueMinor": 5000
},
"merchantReference": "order-1234",
"expiresAt": "2026-04-09T13:00:00Z",
"createdAt": "2026-04-09T12:00:00Z",
"updatedAt": "2026-04-09T12:00:00Z"
}
}About valueMinor: All amounts are in minor currency units (cents for USD). 5000 means $50.00. See the Merchant API Reference for the full schema.
The Idempotency-Key header is required on all write operations (POST, PATCH, DELETE). It prevents duplicate charges if a request is retried. Use a unique value per order, such as your internal order ID.
Step 2: Redirect your customer
Send the customer to the url from the response. Use HTTP 303 so the browser switches from POST to GET.
app.post('/checkout', async (req, res) => {
// ... create the session (see Step 1) ...
res.redirect(303, session.data.url);
});<!-- Or link directly -->
<a href="https://checkout.miracle.com/cs_test_abc123">Pay now</a>Use HTTP 303 (not 301 or 302) to ensure the browser makes a GET request to the checkout page.
Step 3: Customer pays
On the hosted page, the customer selects a payment method and completes the payment. You do not need to handle card details or 3D Secure — Miracle takes care of everything.
For testing in sandbox mode, use this test card:
Card number: 4242 4242 4242 0000
Expiry: any future date
CVC: any 3 digitsAfter payment, the customer is redirected to your successUrl or cancelUrl.
See Test Cards for more scenarios: declines, 3D Secure challenges, and pending payments.
Step 4: Handle the result
Miracle sends a webhook to your server and redirects the customer back to your site. Both happen in parallel.
Always verify payment status via the webhook before fulfilling the order. The customer redirect may arrive before the webhook, and the customer could have manipulated the redirect URL.
Here is a minimal webhook handler:
app.post('/webhooks/miracle', (req, res) => {
// TODO: verify webhook signature (see Webhooks guide)
const { events } = req.body;
for (const event of events) {
switch (event.type) {
case 'payment.succeeded':
// Fulfill the order
break;
case 'payment.failed':
// Notify the customer
break;
case 'payment.requires_action':
// Customer needs to complete 3D Secure — no action needed
// if you are using HPP, Miracle handles this automatically
break;
case 'payment.requires_capture':
// Authorization succeeded (two-step flow) — call
// POST /v1/payments/{id}/capture when ready to collect funds
break;
case 'payment.canceled':
// Payment was canceled — update your order status
break;
}
}
// Always return 200 to acknowledge receipt
res.status(200).send('OK');
});See the Webhooks guide for the full list of event types.
You can also check session status at any time by calling GET /v1/hpp/sessions/{session_id}:
curl https://api.miracle.com/v1/hpp/sessions/cs_test_abc123 \
-H "Authorization: Bearer sk_test_your_secret_key"The status field will be open, completed, or expired. When completed, paymentId and paymentStatus are populated.
What's next
You just accepted your first payment. Here is what to do next:
- Step 3: Handle Webhooks — set up proper webhook signature verification
- HPP Integration Guide — full HPP customization, payment methods, branding
- Test Cards — all test scenarios (declines, 3D Secure, async)
- Go-Live Checklist — everything you need before switching to production