1. Create the payment
Your backend sends the order reference, amount, and optional customer details to ICEPAY.
Learn how to create payments, redirect customers to ICEPAY Checkout, handle payment status updates, issue refunds, and forward funds.
The ICEPAY Checkout API lets you create a payment on your server, send the customer to the hosted ICEPAY Checkout Page, and receive the final payment state back in your application.
This guide explains the complete integration flow first, then goes deeper into payment methods, statuses, webhooks, refunds, payment forwarding, and error handling.
At a high level, your application creates a payment and ICEPAY returns a checkout URL. Your customer completes the payment on ICEPAY, while your backend keeps the payment state synchronized through webhooks.
1. Create the payment
Your backend sends the order reference, amount, and optional customer details to ICEPAY.
2. Redirect the customer
Use the links.checkout URL returned by ICEPAY to redirect the customer to the payment page.
3. Receive status updates
Configure a webhookUrl so ICEPAY can notify your backend when the payment status changes.
4. Confirm the result
Use the webhook state as your source of truth. When the customer returns, you can also retrieve the payment by its ICEPAY payment key.
Customer starts checkout
The customer starts the checkout process in your application.
Create the payment
Your backend sends the payment details to ICEPAY:
POST /api/payments
Receive the checkout URL
ICEPAY creates the payment and returns a payment object containing:
links.checkout
Redirect the customer
Redirect the customer’s browser to links.checkout to continue on the payment page.
Customer completes or stops the payment
After checkout, ICEPAY communicates the result through two separate channels.
You need:
The API uses HTTP Basic Authentication.
Use:
curl --user "$ICEPAY_MERCHANT_ID:$ICEPAY_MERCHANT_SECRET" \ https://checkout.icepay.com/api/payments/methods<?php
$merchantId = getenv('ICEPAY_MERCHANT_ID');$merchantSecret = getenv('ICEPAY_MERCHANT_SECRET');
$ch = curl_init('https://checkout.icepay.com/api/payments/methods');
curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_USERPWD => $merchantId . ':' . $merchantSecret, CURLOPT_HTTPAUTH => CURLAUTH_BASIC,]);
$response = curl_exec($ch);const merchantId = process.env.ICEPAY_MERCHANT_ID;const merchantSecret = process.env.ICEPAY_MERCHANT_SECRET;
const authorization = Buffer .from(`${merchantId}:${merchantSecret}`) .toString('base64');
const response = await fetch( 'https://checkout.icepay.com/api/payments/methods', { headers: { Authorization: `Basic ${authorization}`, }, },);Create a payment with:
POST /api/paymentsOnly reference and amount are required. In most real integrations you should also send a redirectUrl and webhookUrl.
Create an order in your own system
Generate your own order or payment reference first. For example:
ORD-16307This lets you connect the ICEPAY payment back to the correct order in your application.
Build the payment request
{ "reference": "ORD-16307", "description": "Ice cream dipped in sprinkles", "amount": { "value": 299, "currency": "eur" }, "redirectUrl": "https://merchant.example.com/payment-complete", "webhookUrl": "https://merchant.example.com/payment-webhook", "customer": { "email": "johndoe@example.com" }}Send the request from your backend
Send the JSON payload to POST /api/payments using HTTP Basic Authentication.
Request examples are shown directly below this flow.
Store the ICEPAY payment key
A successful response contains a unique payment key such as:
pi-01j1ps8zf4jgnk0c3dnd477sp1Store this key with your own order. You need it when retrieving, refunding, or forwarding the payment.
Redirect the customer to ICEPAY
Use the returned checkout link:
payment.links.checkoutExample:
https://checkout.icepay.com/checkout/pi-01j1ps8zf4jgnk0c3dnd477sp1curl --request POST \ --user "$ICEPAY_MERCHANT_ID:$ICEPAY_MERCHANT_SECRET" \ --header "Content-Type: application/json" \ --data '{ "reference": "ORD-16307", "description": "Ice cream dipped in sprinkles", "amount": { "value": 299, "currency": "eur" }, "redirectUrl": "https://merchant.example.com/payment-complete", "webhookUrl": "https://merchant.example.com/payment-webhook", "customer": { "email": "johndoe@example.com" } }' \ https://checkout.icepay.com/api/payments<?php
$payload = [ 'reference' => 'ORD-16307', 'description' => 'Ice cream dipped in sprinkles', 'amount' => [ 'value' => 299, 'currency' => 'eur', ], 'redirectUrl' => 'https://merchant.example.com/payment-complete', 'webhookUrl' => 'https://merchant.example.com/payment-webhook', 'customer' => [ 'email' => 'johndoe@example.com', ],];
$ch = curl_init('https://checkout.icepay.com/api/payments');
curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPAUTH => CURLAUTH_BASIC, CURLOPT_USERPWD => getenv('ICEPAY_MERCHANT_ID') . ':' . getenv('ICEPAY_MERCHANT_SECRET'), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),]);
$response = curl_exec($ch);const authorization = Buffer .from( `${process.env.ICEPAY_MERCHANT_ID}:${process.env.ICEPAY_MERCHANT_SECRET}`, ) .toString('base64');
const response = await fetch( 'https://checkout.icepay.com/api/payments', { method: 'POST', headers: { Authorization: `Basic ${authorization}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ reference: 'ORD-16307', description: 'Ice cream dipped in sprinkles', amount: { value: 299, currency: 'eur', }, redirectUrl: 'https://merchant.example.com/payment-complete', webhookUrl: 'https://merchant.example.com/payment-webhook', customer: { email: 'johndoe@example.com', }, }), },);
const payment = await response.json();Amounts use minor currency units and must be sent as integers.
For a currency with two decimal places:
| Customer-facing amount | API value |
|---|---|
| €0.01 | 1 |
| €2.99 | 299 |
| €10.00 | 1000 |
| €149.95 | 14995 |
| Field | Required | Description |
|---|---|---|
reference |
Yes | Your payment or order reference. Maximum 255 characters. |
amount.value |
Yes | Payment amount in minor currency units. Minimum 1. |
amount.currency |
Yes | Supported three-letter currency code, for example eur. |
description |
No | Description shown during checkout. If omitted, the reference may be used. |
paymentMethod |
No | Preselects a payment method instead of letting the customer choose on the Checkout Page. |
redirectUrl |
No | HTTPS URL to which the customer returns when the payment is completed or stopped. |
webhookUrl |
No | HTTPS URL to which ICEPAY sends payment status updates. |
customer |
No | Customer email, name, IP address, and address details. |
expireAfter |
No | Payment lifetime in minutes. Defaults to four hours. |
Both redirectUrl and webhookUrl must use HTTPS when supplied.
Payments remain open for four hours by default.
Use expireAfter to change this:
{ "expireAfter": 1440}1440 means 24 hours.
The maximum value is 44640 minutes, which is 31 days.
If you omit paymentMethod, ICEPAY lets the customer select an available payment method on the hosted Checkout Page.
{ "reference": "ORD-16307", "amount": { "value": 299, "currency": "eur" }}For a simple checkout integration, this is usually the least complex approach because your application does not need to build its own payment-method selector.
If your own checkout already lets customers choose a payment method, send its identifier when creating the payment:
{ "reference": "ORD-16307", "amount": { "value": 299, "currency": "eur" }, "paymentMethod": { "type": "ideal" }}The OpenAPI document defines these payment method identifiers:
idealbancontactonlineueberweisencardpaypalepspaybybankHowever, the methods actually available depend on the merchant configuration.
Use:
GET /api/payments/methodsExample response:
[ { "id": "ideal", "description": "iDEAL | Wero" }, { "id": "paypal", "description": "PayPal" }, { "id": "card", "description": "Card" }, { "id": "bancontact", "description": "Bancontact" }]A created or retrieved payment contains both the operational payment state and information that helps your application continue the checkout flow.
A shortened example:
{ "key": "pi-01j1ps8zf4jgnk0c3dnd477sp1", "status": "started", "financialStatus": "uncleared", "amount": { "value": 299, "currency": "eur" }, "paymentMethod": null, "reference": "ORD-16307", "isTest": true, "refunds": [], "createdAt": "2024-07-01T09:16:06.500000Z", "expiresAt": "2024-07-01T13:16:06.433802Z", "updatedAt": "2024-07-01T09:16:06.500000Z", "links": { "checkout": "https://checkout.icepay.com/checkout/pi-01j1ps8zf4jgnk0c3dnd477sp1", "documentation": "https://docs.icepay.com" }}status and financialStatus are differentThe payment contains two status fields with different meanings.
| Field | Possible values | Meaning |
|---|---|---|
status |
started, pending, completed, expired, cancelled |
The current lifecycle state of the payment. |
financialStatus |
uncleared, cleared |
Whether the payment funds have been received. |
Do not treat these two properties as interchangeable.
startedThe payment has been created and checkout can begin.
pendingThe payment is still being processed and does not yet have a final state.
completedThe payment has completed.
expiredThe payment expired before it reached a successful final state.
cancelledThe payment was cancelled.
A webhook is the server-to-server notification ICEPAY sends to your webhookUrl when the payment status changes.
This is important because the customer returning to your website is a browser event. A webhook is a backend event and does not depend on the customer keeping the browser open or successfully returning to your site.
Expose an HTTPS endpoint
For example:
https://merchant.example.com/payment-webhookRead the raw JSON request body
ICEPAY signs the raw JSON body, so signature verification should use the body exactly as it was received.
Read the ICEPAY-Signature request header
The signature is a Base64-encoded SHA-256 HMAC generated with your Merchant Secret.
Calculate the expected signature
Conceptually:
Base64( HMAC-SHA256( raw request body, Merchant Secret ))Compare the received and expected signatures
Use a timing-safe comparison where your language or framework provides one.
Process the payment state
The webhook payload has the same payment-shaped structure used elsewhere in the API.
Use the payment key to locate the payment in your database, then update the stored status.
<?php
$rawBody = file_get_contents('php://input');$receivedSignature = $_SERVER['HTTP_ICEPAY_SIGNATURE'] ?? '';
$expectedSignature = base64_encode( hash_hmac( 'sha256', $rawBody, getenv('ICEPAY_MERCHANT_SECRET'), true, ),);
if (!hash_equals($expectedSignature, $receivedSignature)) { http_response_code(401); exit;}
$payment = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
// Update your payment using $payment['key'] and $payment['status'].import crypto from 'node:crypto';
// `rawBody` must be the request body before JSON parsing.const expectedSignature = crypto .createHmac('sha256', process.env.ICEPAY_MERCHANT_SECRET) .update(rawBody) .digest('base64');
const receivedSignature = request.headers['icepay-signature'] ?? '';
const valid = receivedSignature.length === expectedSignature.length && crypto.timingSafeEqual( Buffer.from(receivedSignature), Buffer.from(expectedSignature), );
if (!valid) { response.statusCode = 401; response.end(); return;}
const payment = JSON.parse(rawBody);The OpenAPI document defines the webhook payload and its signature, but does not define a delivery or retry policy.
For that reason, design your handler so receiving the same payment state more than once is safe.
For example:
1. Find payment by ICEPAY payment key.2. Verify the webhook signature.3. Compare the received state with your stored state.4. Update the payment inside a database transaction.5. Trigger fulfilment only when your own fulfilment condition changes from false to true.This prevents duplicate business actions such as sending the same order twice.
When creating a payment you can supply:
{ "redirectUrl": "https://merchant.example.com/payment-complete"}ICEPAY redirects the customer there after the payment is completed or stopped.
The return page is useful for the customer experience, but it should not replace webhook processing.
A robust return-page flow is:
Retrieve the latest payment representation with:
GET /api/payments/{key}Example:
curl --user "$ICEPAY_MERCHANT_ID:$ICEPAY_MERCHANT_SECRET" \ https://checkout.icepay.com/api/payments/pi-01j1ps8zf4jgnk0c3dnd477sp1This returns the Payment object.
Use this endpoint when:
For ongoing payment status updates, use webhooks.
You can include customer data when creating the payment:
{ "customer": { "email": "johndoe@example.com", "firstName": "Jane", "lastName": "Doe", "ip": "127.0.0.1", "address": { "streetName": "Orlyplein", "houseNumber": "77", "postalCode": "1043 DS", "city": "Amsterdam", "province": "Noord-Holland", "country": "nl" } }}The address country uses an ISO 3166 alpha-2 country code such as nl.
Customer information is returned under the payment’s meta.customer object.
Create a refund with:
POST /api/payments/{key}/refundThe request requires:
reference;The currency is inherited from the original payment and does not need to be supplied.
Example:
{ "reference": "RFD-00069", "description": "Received chocolate dip instead of sprinkles", "amount": { "value": 299 }}curl --request POST \ --user "$ICEPAY_MERCHANT_ID:$ICEPAY_MERCHANT_SECRET" \ --header "Content-Type: application/json" \ --data '{ "reference": "RFD-00069", "description": "Received chocolate dip instead of sprinkles", "amount": { "value": 299 } }' \ https://checkout.icepay.com/api/payments/pi-01j1ps8zf4jgnk0c3dnd477sp1/refundconst response = await fetch( `https://checkout.icepay.com/api/payments/${paymentKey}/refund`, { method: 'POST', headers: { Authorization: `Basic ${authorization}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ reference: 'RFD-00069', description: 'Received chocolate dip instead of sprinkles', amount: { value: 299, }, }), },);A refund has its own key and can have a status of:
pendingcompletedThe payment representation also contains a refunds array with refund summaries.
ICEPAY also defines a payment-forwarding operation:
POST /api/payments/{key}/forwardThis forwards part of a payment to another ICEPAY merchant.
Before you use it:
financialStatus: "cleared".Example request:
{ "reference": "FWD-00042", "description": "Forwarding to merchant 1001", "recipient": { "id": "1001" }, "amount": { "value": 299 }}The destination ICEPAY merchant ID is supplied as recipient.id.
A forwarding request has its own key and can have a status of:
pendingcompletedThe payment representation can contain a forwards array with forwarding summaries.
The API defines the following HTTP error responses:
| Status | Meaning |
|---|---|
400 |
The request is invalid. |
401 |
Authentication failed. |
404 |
The requested resource was not found. |
422 |
The request could not be processed. |
500 |
An unexpected server error occurred. |
Error responses use a flexible structure that can contain:
{ "message": "Invalid request.", "errors": {}}Because the exact error fields may vary by endpoint, your integration should not depend on one rigid error payload beyond the documented common fields.
A practical handler can branch primarily on the HTTP status:
if (response.ok) { return response.json();}
const error = await response.json().catch(() => null);
switch (response.status) { case 400: // Invalid request. break; case 401: // Check Merchant ID and Merchant Secret. break; case 404: // Check the payment key. break; case 422: // Request was understood but could not be processed. break; default: // Unexpected or server-side failure. break;}The API does not prescribe how your application stores payments, but keeping your own order data and the ICEPAY payment state together makes the integration easier to maintain.
| Field | Example | Purpose |
|---|---|---|
id |
4821 |
Internal payment record ID. |
order_id |
16307 |
Links the payment to your order. |
provider |
icepay |
Identifies the payment provider. |
provider_payment_key |
pi-... |
The unique payment key returned by ICEPAY. |
reference |
ORD-16307 |
Your payment or order reference. |
amount_value |
299 |
Payment amount in minor currency units. |
currency |
eur |
Payment currency. |
status |
started |
Current ICEPAY payment status. |
financial_status |
uncleared |
Whether the payment funds have been received. |
payment_method |
null |
Payment method used for the payment, when known. |
expires_at |
2024-07-02T.. |
When the ICEPAY payment expires. |
provider_created_at |
2024-07-01T.. |
When ICEPAY created the payment. |
provider_updated_at |
2024-07-01T.. |
When ICEPAY last updated the payment. |
updated_at |
2024-07-01T.. |
Your application’s local update timestamp. |
Before relying on the integration in a live checkout, verify that you:
redirectUrl and webhookUrl;ICEPAY-Signature before trusting webhook data;status and financialStatus as separate concepts;400, 401, 404, 422, and 500 responses;