Checkout API
Authentication
To authenticate requests to the Checkout API we use Basic authentication
Your Merchant ID is used as the username
, your Merchant Secret is used as the password
Both of these values can be obtained from the ICEPAY Portal
They are then added into an Authorization header using the format Basic <base64 username:password\>
Example
Merchant ID is 10000
Merchant Secret is xxxxxx
The base64 encoding of the password and username, 10000:xxxxxx, will result in MTAwMDA6eHh4eHh4
Authentication is required for all request.
An ICEPAY Merchant can be set to test or live processing mode via the ICEPAY Portal
For an explanation, please see our Testing page
Creating a checkout
When creating a checkout no payment method has to be provided.
The customer will be able to choose this on the ICEPAY Checkout Page
POST: https://checkout.icepay.com/api/payments
{
"reference": "OR-123412", // Required string, max length of 255 characters
"description": "Ice cream with sprinkles", // optional string, max length of 255 characters, visible in the checkout. If absent the reference is used as the description.
"amount": {
"value": 299, // Required integer, in minor units and above zero
"currency": "eur" // Required enum
},
"redirectUrl": "https://<redirectUrl>", // optional string, max length of 512 characters and https. This url is used to sent the customer back when the payment is completed or stopped
"webhookUrl": "https://<postbackUrl>" // optional string, maximum length of 512 and https.
}
Put a reference in the redirectUrl & webhookUrl
You can customize the redirect and webhook urls. For example you can put your order id in the urls.
curl --silent --location --request POST 'https://checkout.icepay.com/api/payments' \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic MTAwMDA6eHh4eHh4' \
--data '{
"reference": "ORD-16307",
"description": "Icecream dipped in Sprinkels",
"amount": {
"value": 299,
"currency": "eur"
},
"redirectUrl": "https://<redirectUrl>",
"webhookUrl": "https://<postbackUrl>"
}'
$merchantId = '10000';
$merchantSecret = 'xxxxxx';
$body = [
'reference' => 'ORD-16307',
'description' => 'Ice cream dipped in Sprinkles',
'amount' => [
'value' => 299,
'currency' => 'eur'
],
'webhookUrl' => 'https://<redirectUrl>',
'redirectUrl' => 'https://<postbackUrl>',
];
$response = Http::withBasicAuth($merchantId, $merchantSecret)
->post('https://checkout.icepay.com/api/payments', $body);
Log::debug($response->json());
const merchantId = '10000';
const merchantSecret = 'xxxxxx';
const headers = {
'Content-Type': 'application/json',
'Authorization': `Basic ${btoa(`${merchantId}:${merchantSecret}`)}`
};
const body = JSON.stringify({
"reference": "ORD-16307",
"description": "Icecream dipped in Sprinkels",
"amount": {
"value": 299,
"currency": "eur"
},
"webhookUrl": "https://<postbackUrl>",
"redirectUrl": "https://<redirectUrl>",
});
const result = await fetch('https://checkout.icepay.com/api/payments', {
method: 'POST',
headers: headers,
body: body,
});
console.log(await result.json());
Example response when a payment has been successfully created:
{
"key": "pi-01j1ps8zf4jgnk0c3dnd477sp1",
"status": "started",
"amount": {
"value": 299,
"currency": "eur"
},
"paymentMethod": null,
"description": "Ice cream dipped in Sprinkles",
"reference": "ORD-16307",
"webhookUrl": "https://<redirectUrl>",
"redirectUrl": "https://<postbackUrl>",
"merchant": {
"id": 10000,
"name": "IJs winkel"
},
"isTest": false,
"refunds": [],
"createdAt": "2024-07-01T09:16:06.500000Z",
"expiresAt": "2024-07-01T13:16:06.433802Z",
"updatedAt": "2024-07-01T09:16:06.500000Z",
"meta": null,
"links": {
"checkout": "https://checkout.icepay.com/checkout/pi-01j1ps8zf4jgnk0c3dnd477sp1",
"documentation": "https://docs.icepay.com"
}
}
The customer can be redirected to the checkout url in the links object to complete the payment. The key provided in the response is the unique identifier of the payment. This key is also used to create a refund or to retrieve the payment.
Extend payment
By default, the payment will stay open for 4 hours. By providing the expireAfter
property this be extended up to 31 days.
The expireAfter property will take the amount of time it will stay open in minutes as an integer between 0 and 44640. This can be useful when, for example, printing the Checkout URL as QR code on an invoice.
Providing a payment method
When the customer chooses a payment method in your checkout the request body can be modified to include a paymentMethod object.
POST: https://checkout.icepay.com/api/payments
{
"reference": "ORD-16307", // Required string, max length of 255 characters
"description": "Ice cream dipped in Sprinkles", // optional string, max length of 255 characters, visible in the checkout. If absent the reference is used a the description.
"amount": {
"value": 299, // Required integer, in minor units and above zero
"currency": "eur" // Required enum
},
"paymentMethod": {
"type": "card" // Optional enum: ideal, card, bancontact, paypal etc.
},
"redirectUrl": "https://<redirectUrl>", // optional string, max length of 512 characters and https. This url is used to sent the customer back when the payment is completed or stopped
"webhookUrl": "https://<postbackUrl>" // optional string, maximale lengte 512 en https.
}
Payment Method | Enum |
---|---|
Bancontact | bancontact |
iDEAL | ideal |
Online Überweisen | onlineueberweisen |
Visa & Mastercard | card |
PayPal | paypal |
EPS | EPS |
When redirecting the customer use the 'direct' url from the links object instead of the 'checkout' url to skip the ICEPAY Checkout Page.
Example response when a payment has been successfully created with a provided payment method:
{
"key": "pi-01j1pta7ymwcjk25q4rtpqmn2q",
"status": "started",
"amount": {
"value": 299,
"currency": "eur"
},
"paymentMethod": null,
"description": "Ice cream dipped in Sprinkles",
"reference": "ORD-16307",
"webhookUrl": "https://<redirectUrl>",
"redirectUrl": "https://<postbackUrl>",
"merchant": {
"id": 10000,
"name": "IJs winkel"
},
"isTest": false,
"refunds": [],
"createdAt": "2024-07-01T09:34:16.532000Z",
"expiresAt": "2024-07-01T13:34:16.470510Z",
"updatedAt": "2024-07-01T09:34:16.532000Z",
"meta": null,
"links": {
"direct": "https://card.icepay.com/card/<key>",
"checkout": "https://checkout.icepay.com/checkout/pi-01j1pta7ymwcjk25q4rtpqmn2q",
"documentation": "https://docs.icepay.com"
}
}
Obtaining payment methods
GET: https://checkout.icepay.com/api/payments/methods
curl --silent --location --request GET 'https://checkout.icepay.com/api/payments/methods' \
--header 'Authorization: Basic MTAwMDA6eHh4eHh4'
$merchantId = '10000';
$merchantSecret = 'xxxxxx';
$response = Http::withBasicAuth($merchantId, $merchantSecret)
->get('https://checkout.icepay.com/api/payments/methods');
Log::debug($response->json());
const merchantId = '10000';
const merchantSecret = 'xxxxxx';
const headers = {
'Content-Type': 'application/json',
'Authorization': `Basic ${btoa(`${merchantId}:${merchantSecret}`)}`
};
const result = await fetch('https://checkout.icepay.com/api/payments/methods', {
method: 'GET',
headers: headers,
});
console.log(await result.json());
The response wil be a list of objects with IDs and descriptions for the payment methods. The ID can be used as an id on the type in the paymentMethod object when creating a direct payment.
Example get payment methods call:
[
{
"id": "ideal",
"description": "iDEAL"
},
{
"id": "paypal",
"description": "PayPal"
},
{
"id": "card",
"description": "Card"
},
{
"id": "bancontact",
"description": "Bancontact"
}
]
Status
When the status of a Checkout changes we send a request to the provided webhook to inform you about the change.
flowchart LR
S[started] --> C[completed]
S[started] --> P[pending]
S[started] --> E[expired]
S[started] --> CA[cancelled]
P --> C
P --> S
E --> C
E --> P
The initial status of a payment is started. When cancelling a payment, no postback is provided.
When the customer pays the status goes to completed. For some payment methods it can take a little while before we receive confirmation that the payment has been successful. In this case we set the status to pending. If the payment was not successfully we change the status back to started, if it is successful we change the status to completed.
If the payment has not been completed within its timeframe, the status will change to expired.
Generally speaking an expired payment will not change status. However, if a customer is, for example, on the iDEAL payment page and completes the payment we will change the status to completed.
Payment flow
sequenceDiagram
participant App as Webshop
participant Api as Checkout API
participant Page as Payment Page
App->>Api: Create a payment
Api->>App: Returns the urls for the next step
App->>Page: Redirect the customer to the payment page (or checkout)
Note over Page: Customer Completes payment
Page->>Api: Customer redirects back to ICEPAY
loop Until a 200 response <br> or multiple attempts spread over 24h
Api-->>App: We send a postback if a webhookUrl is provided
end
Api->>App: Customer is send to the redirectUrl
Note over App: Once the customer returns check <br> for the status of the payment. If the payment has been marked <br> completed in your system the postback has been resolved, <br> otherwise retrieve the status from the API to verify the status of the payment
Retrieving a payment
GET: https://checkout.icepay.com/api/payments/{key}
curl --silent --location --request GET 'https://checkout.icepay.com/api/payments/{key}' \
--header 'Authorization: Basic MTAwMDA6eHh4eHh4'
$merchantId = '10000';
$merchantSecret = 'xxxxxx';
$response = Http::withBasicAuth($merchantId, $merchantSecret)->get('https://checkout.icepay.com/api/payments/{key}');
Log::debug($response->json());
const merchantId = '10000';
const merchantSecret = 'xxxxxx';
const headers = {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + btoa(`${merchantId}:${merchantSecret}`)
};
const result = await fetch('https://checkout.icepay.com/api/payments/{key}', {
method: 'GET',
headers: headers,
});
console.log(await result.json());
The following is an example response of a payment:
{
"key": "pi-01j1pta7ymwcjk25q4rtpqmn2q",
"status": "started",
"amount": {
"value": 299,
"currency": "eur"
},
"paymentMethod": null,
"description": "Ice cream dipped in Sprinkles",
"reference": "ORD-16307",
"webhookUrl": "https://<redirectUrl>",
"redirectUrl": "https://<postbackUrl>",
"merchant": {
"id": 10000,
"name": "IJs winkel"
},
"isTest": false,
"refunds": [],
"createdAt": "2024-07-01T09:34:16.533000Z",
"expiresAt": "2024-07-01T13:34:16.470000Z",
"updatedAt": "2024-07-01T09:34:16.533000Z",
"meta": null,
"links": {
"checkout": "https://checkout.icepay.com/checkout/pi-01j1pta7ymwcjk25q4rtpqmn2q",
"documentation": "https://docs.icepay.com"
}
}
Resolving the webhook
When a payment changes status, a request is sent to the provided webhook url. The data received from the webhook is structured the same as the response from a GET request. The request has an ICEPAY-Signature header. This header contains a base64 encode sha256 hmac with the merchant secret as the secret and the body json encoded as the data. Please note that it can, but is not guaranteed to, contain escapes slashes (\/).
$merchantSecret = 'xxxxxx';
$signature = $request->header('ICEPAY-Signature');
$calculatedSignature = base64_encode(hash_hmac('sha256', json_encode($body), $merchantSecret, true));
Log::debug( $signature === $calculatedSignature);
import {createHmac} from 'crypto';
// ...
const merchantSecret = 'xxxxxx';
const signature = ''; // get the request header in utf8
// our signature does contain \/ if hardcoding the body during testing use the String.raw`` template literal
const body = ''; // get the request body in utf8
const calculatedSignature = createHmac('sha256', merchantSecret)
.update(body)
.digest('base64');
console.log(signature === calculatedSignature);
Example webhook body:
{
"key": "pi-01j1pta7ymwcjk25q4rtpqmn2q",
"status": "started",
"amount": {
"value": 299,
"currency": "eur"
},
"paymentMethod": null,
"description": "Ice cream dipped in Sprinkles",
"reference": "ORD-16307",
"webhookUrl": "https://<redirectUrl>",
"redirectUrl": "https://<postbackUrl>",
"merchant": {
"id": 10000,
"name": "IJs winkel"
},
"isTest": false,
"refunds": [],
"createdAt": "2024-07-01T09:34:16.533000Z",
"expiresAt": "2024-07-01T13:34:16.470000Z",
"updatedAt": "2024-07-01T09:34:16.533000Z",
"meta": null,
"links": {
"checkout": "https://checkout.icepay.com/checkout/pi-01j1pta7ymwcjk25q4rtpqmn2q",
"documentation": "https://docs.icepay.com"
}
}
If no payment method was provided when creating the checkout, the postback will also show the payment method used to complete the payment.
Contact us if you want to whitelist the IPs of our servers.
Refunding a payment
POST https://checkout.icepay.com/api/payments/{id}/refund
{
"reference": "RFD-00069", // Required string, max length of 255 characters
"description": "Received choco dip instead of sprinkles", // Optional string, max length of 255 characters
"amount": {
"value": 299, // Required integer, in minor units and above zero
// "currency" is not needed, we use the same as the original payment
}
}
curl --silent --location --request POST 'https://checkout.icepay.com/api/payments/{key}/refund' \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic MTAwMDA6eHh4eHh4' \
--data '{
"reference": "RFD-00069",
"description": "Received choco dip instead of sprinkles",
"amount": {
"value": 299
}
}'
$merchantId = '10000';
$merchantSecret = 'xxxxxx';
$intentKey = 'pi-01j1pta7ymwcjk25q4rtpqmn2q';
$body = [
'reference' => 'RFD-00069',
'description' => 'Received choco dip instead of sprinkles',
'amount' => [
'value' => 299,
],
];
$response = Http::withBasicAuth($merchantId, $merchantSecret)
->post('https://checkout.icepay.com/api/payments/' . $intentKey . '/refund', $body);
Log::debug($response->json());
const merchantId = '10000';
const merchantSecret = 'xxxxxx';
const intentKey = 'pi-01j1pta7ymwcjk25q4rtpqmn2q';
const headers = {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + btoa(`${merchantId}:${merchantSecret}`)
};
const body = JSON.stringify({
'reference': 'RFD-00069',
'description': 'Received choco dip instead of sprinkles',
'amount': {
'value': 299,
}
});
const result = await fetch(`https://checkout.icepay.com/api/payments/${intentKey}/refund`, {
method: 'POST',
headers: headers,
body: body,
});
console.log(await result.json());
After creating a refund the following response can be expected:
{
"key": "pr-01j1sr38kspvq2z5xek20y565w",
"status": "completed",
"amount": {
"value": 299,
"currency": "eur"
},
"description": "Received choco dip instead of sprinkles",
"reference": "RFD-00069",
"payment": {
"key": "pi-01j1sr24qph973tf3hgsvqe7h7",
"status": "completed",
"amount": {
"value": 299,
"currency": "eur"
},
"paymentMethod": {
"type": "ideal"
},
"description": "Ice cream dipped in Sprinkles",
"reference": "ORD-16307",
"redirectUrl": "https://<redirect>",
"webhookUrl": "https://<postback>",
"merchant": {
"id": 10000,
"name": "IJsje van ICEPAY"
},
"isTest": true,
"createdAt": "2024-07-02T12:52:37.000000Z",
"expiresAt": "2024-07-02 16:52:37",
"updatedAt": "2024-07-02T12:53:07.000000Z",
"meta": null,
"links": {
"checkout": "https://checkout.icepay.com/checkout/pi-01j1sr38kspvq2z5xek20y565w",
"documentation": "https://docs.icepay.com"
}
},
"createdAt": "2024-07-02T12:53:13.000000Z",
"updatedAt": "2024-07-02T12:53:13.000000Z"
}
Code | Status |
---|---|
200-299 | Successful |
400-499 | Error in the request |
500-599 | Contact ICEPAY |
Most of the time the status of the refund wil go to completed. If the payment method requires some time to accept refund set the status to pending. If the refund was unsuccessful or declined the status wil be set to failed.
If the refund has a status of pending we will send an update via the webhook url. In the body will be a refunds property with the current status of the refunds.
Example refund postback:
{
"id": "pi-01j1pta7ymwcjk25q4rtpqmn2q",
"status": "completed",
"amount": {
"value": 299,
"currency": "eur"
},
"paymentMethod": {
"type": "ideal"
},
"description": "Ice cream dipped in Sprinkles",
"reference": "ORD-16307",
"redirectUrl": "https://<redirect>",
"webhookUrl": "https://<postback>",
"merchant": {
"id": 10000,
"name": "IJsje van ICEPAY"
},
"isTest": false,
"createdAt": "2024-07-01T10:26:01.000000Z",
"expiresAt": "2024-07-01T13:34:16.470000Z",
"updatedAt": "2024-07-01T09:34:16.533000Z",
"refunds": [
{
"id": "pr-01j1sr38kspvq2z5xek20y565w",
"status": "completed",
"amount": {
"value": 299,
"currency": "eur"
},
"description": "Received choco dip instead of sprinkles",
"reference": "RFD-00069",
"expiresAt": "2024-07-01T13:34:16.470000Z",
"updatedAt": "2024-07-01T09:34:16.533000Z"
}
],
"links": {
"checkout": "https://checkout.icepay.com/checkout/pi-01j1sr38kspvq2z5xek20y565w",
"documentation": "https://docs.icepay.com"
}
}
Forwarding a payment
What is Payment forwarding?
With Payment Forward, you can transfer part of the funds to another ICEPAY Account. Read more on the Payment Forwarding page
Payment Forwarding is only available if this product is activated on your ICEPAY Account. Contact ICEPAY to activate it.
POST https://checkout.icepay.com/api/payments/{id}/forward
{
"reference": "FWD-00042", // Required string, max length of 255 characters
"description": "Forwarding to merchant 1001", // Optional string, max length of 255 characters
"recipient": {
"id" : "1001" // Required, valid Merchant ID
},
"amount": {
"value": 299, // Required integer, in minor units and above zero
// "currency" is not needed, we use the same as the original payment
}
}
curl --silent --location --request POST 'https://checkout.icepay.com/api/payments/{key}/forward' \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic MTAwMDA6eHh4eHh4' \
--data '{
"reference": "FWD-00042",
"description": "Forwarding to merchant 1001",
"recipient": {
"id" : "1001"
},
"amount": {
"value": 299
}
}'
$merchantId = '10000';
$merchantSecret = 'xxxxxx';
$intentKey = 'pi-01j1pta7ymwcjk25q4rtpqmn2q';
$body = [
'reference' => 'FWD-00042',
'description' => 'Forwarding to merchant 1001',
'recipient' => [
'id' => '1001',
],
'amount' => [
'value' => 299,
],
];
$response = Http::withBasicAuth($merchantId, $merchantSecret)
->post('https://checkout.icepay.com/api/payments/' . $intentKey . '/forward', $body);
Log::debug($response->json());
const merchantId = '10000';
const merchantSecret = 'xxxxxx';
const intentKey = 'pi-01j1pta7ymwcjk25q4rtpqmn2q';
const headers = {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + btoa(`${merchantId}:${merchantSecret}`)
};
const body = JSON.stringify({
"reference": "FWD-00042",
"description": "Forwarding to merchant 1001",
"recipient": {
"id" : "1001"
},
"amount": {
"value": 299,
}
});
const result = await fetch(`https://checkout.icepay.com/api/payments/${intentKey}/forward`, {
method: 'POST',
headers: headers,
body: body,
});
console.log(await result.json());
After forwarding a payment the following response can be expected:
{
"key": "pf-01j1sr38kspvq2z5xek20y565w",
"status": "completed",
"amount": {
"value": 299,
"currency": "eur"
},
"description": "Forwarding to merchant 1001",
"reference": "FWD-00042",
"recipient": {
"id" : "1001"
},
"payment": {
"key": "pi-01j1sr24qph973tf3hgsvqe7h7",
"status": "completed",
"amount": {
"value": 299,
"currency": "eur"
},
"paymentMethod": {
"type": "ideal"
},
"description": "Ice cream dipped in Sprinkles",
"reference": "ORD-16307",
"redirectUrl": "https://<redirect>",
"webhookUrl": "https://<postback>",
"merchant": {
"id": 10000,
"name": "IJsje van ICEPAY"
},
"isTest": true,
"createdAt": "2024-07-02T12:52:37.000000Z",
"expiresAt": "2024-07-02 16:52:37",
"updatedAt": "2024-07-02T12:53:07.000000Z",
"meta": null,
"links": {
"checkout": "https://checkout.icepay.com/checkout/pi-01j1sr38kspvq2z5xek20y565w",
"documentation": "https://docs.icepay.com"
}
},
"createdAt": "2024-07-02T12:53:13.000000Z",
"updatedAt": "2024-07-02T12:53:13.000000Z"
}
Code | Status |
---|---|
200-299 | Successful |
400-499 | Error in the request |
500-599 | Contact ICEPAY |
Most of the time the status of the forward wil go to completed. If the payment method requires some time to accept the forward set the status to pending. If the forward was unsuccessful or declined the status wil be set to failed.
If the forward has a status of pending we will send an update via the webhook url. In the body will be a forwards property with the current status of the forwards.
Example forward postback:
{
"id": "pi-01j1pta7ymwcjk25q4rtpqmn2q",
"status": "completed",
"amount": {
"value": 299,
"currency": "eur"
},
"paymentMethod": {
"type": "ideal"
},
"description": "Ice cream dipped in Sprinkles",
"reference": "ORD-16307",
"redirectUrl": "https://<redirect>",
"webhookUrl": "https://<postback>",
"merchant": {
"id": 10000,
"name": "IJsje van ICEPAY"
},
"isTest": false,
"createdAt": "2024-07-01T10:26:01.000000Z",
"expiresAt": "2024-07-01T13:34:16.470000Z",
"updatedAt": "2024-07-01T09:34:16.533000Z",
"forwards": [
{
"id": "pf-01j1sr38kspvq2z5xek20y565w",
"status": "completed",
"recipient": {
"id" : "1001"
},
"amount": {
"value": 299,
"currency": "eur"
},
"description": "Forwarding to merchant 1001",
"reference": "FWD-00042",
"expiresAt": "2024-07-01T13:34:16.470000Z",
"updatedAt": "2024-07-01T09:34:16.533000Z"
}
],
"links": {
"checkout": "https://checkout.icepay.com/checkout/pi-01j1sr38kspvq2z5xek20y565w",
"documentation": "https://docs.icepay.com"
}
}