Ealyx Pay Integration
This path covers Phase 3a (Payment via Ealyx Pay) and Phase 4 (Post-Purchase Updates). Choose this path if you want Ealyx to process payments with instant trade-in discounts.
Complete Common Phases (1-2) before implementing this path.
Phase 3a: Payment via Ealyx Pay
Flow Diagram:
Ealyx Pay is the "umbrella" or "abstract" payment method that allows the shopper to pay with their trade-in, through one of the concrete options that Ealyx offers. The Ealyx WebApp will guide the shopper through the payment process, and will offer the shopper the choice of one of the concrete options. You need to make a number of smallish integrations to make it all work:
- show the Ealyx Pay option at checkout,
- intercept the form submission to open the Ealyx Modal for payment,
- prepare the order for later confirmation (webhook), and
- handle the webhook notification.
Step 1: Show the Ealyx Pay option at checkout
You add the Ealyx Pay option to the payment methods list using the following code in the checkout page. Note that
the data-module-name="ealyx" attribute is used to identify the Ealyx Pay option to the SDK.
<label>
<input type="radio" name="payment_method" data-module-name="ealyx" value="ealyx_pay" />
Pay with Ealyx
</label>
Step 2: Make the Ealyx Pay option show relevant information to the shopper
You listen for the Ealyx Pay option selection using the following code in the checkout page. Note that
input[name="payment_method"] needs to refer to the name of the payment method input field. This listener allows
the SDK to know when the Ealyx Pay option is selected and show relevant information to the shopper.
const paymentRadios = document.querySelectorAll('input[name="payment_method"]');
paymentRadios.forEach(radio => {
radio.addEventListener("change", () => {
const isEalyx = radio.value === "ealyx_pay";
window.Ealyx?.updatePaymentSelected(isEalyx);
});
});
Step 3: Intercept Form Submission to open the Ealyx Pay modal
You intercept the form submission to open the Ealyx Pay modal using the following code in the checkout page. Note
that input[name="payment_method"] needs to refer to the name of the payment method input field. This interceptor
allows the SDK to open the Ealyx Pay modal instead of submitting the form to the shop backend.
This code assumes that the form can only be submitted when all relevant T&C checkboxes are checked. If your submit button is always enabled, you need to modify the code to check for the T&C checkboxes.
document.querySelector("form").addEventListener("submit", function (e) {
const selected = document.querySelector('input[name="payment_method"]:checked');
const isEalyx = selected?.value === "ealyx_pay";
if (isEalyx) {
e.preventDefault();
window.Ealyx?.openPayment(); // Launch Ealyx modal
return false;
}
});
Note: The
window.ealyxConfiguration.confirmUrlproperty is used as the redirect URL after the Ealyx Pay payment is completed.
Step 4: Implement Purchase Data Endpoint
When the Ealyx Pay modal opens, it needs the cart and purchase-related data to display to the shopper. Create a GET endpoint at $MERCHANT_CALLBACKS_BASE/purchase_data on your backend.
Who calls this endpoint: The Ealyx frontend SDK (running in the customer's browser in same domain of web) calls this endpoint via JavaScript AJAX to fetch current cart and purchase details for display.
Response Format:
Your endpoint must return JSON with complete purchase data:
{
"purchase": {
"cart": {
"id": "cart_123",
"currency": "EUR",
"items": [
{
"id": "item_1",
"name": "iPhone 15 Pro",
"description": "Excellent condition",
"url": "https://yoursite.com/products/iphone-15-pro",
"quantity": 1,
"price": 89999,
"tax": 18900,
"total": 108899
}
],
"price": 89999,
"tax": 18900,
"discounts": [
{
"id": "discount_1",
"name": "Trade-in discount",
"total": 15000
}
],
"total": 74999
},
"billing": {
"given_name": "John",
"family_name": "Doe",
"phone": "+1234567890",
"address": "123 Main St",
"city": "New York",
"state": "NY",
"country_code": "US",
"postal_code": "10001"
},
"shipping": {
"address": {
"given_name": "John",
"family_name": "Doe",
"phone": "+1234567890",
"address": "123 Main St",
"address2": "Apt 4B",
"city": "New York",
"state": "NY",
"country_code": "US",
"country": "United States",
"postal_code": "10001",
"company": "ACME Corp",
"vat_number": "US123456789",
"notes": "Deliver between 9-5"
},
"method": {
"name": "Standard Shipping",
"provider": "FedEx",
"currency": "EUR",
"price": 1500,
"tax": 315,
"total": 1815
}
},
"customer": {
"id": "customer_123",
"email": "john@example.com",
"given_name": "John",
"family_name": "Doe"
}
}
}
Important Notes:
- All monetary values are in cents (multiply by 100). Example: €899.99 = 89999
cart.price= subtotal without discounts and taxescart.tax= tax amount onlycart.total= price - discounts (without tax)- Each item's
total= (price * quantity) + tax - Billing and shipping addresses are optional
- Customer data is optional
Example Implementation:
# Pseudo-code for purchase data endpoint
# Get cart from session
cart_id = get_session_cart_id()
if not cart_id:
return {'purchase': None}
# Build cart object
cart = {
'id': cart_id,
'currency': 'EUR',
'items': [
{
'id': item.id,
'name': item.name,
'description': item.description,
'url': item.url,
'quantity': item.quantity,
'price': item.unit_price * 100,
'tax': item.tax * 100,
'total': (item.unit_price * item.quantity + item.tax) * 100
}
for item in get_cart_items()
],
'price': cart.subtotal * 100,
'tax': cart.tax * 100,
'discounts': [
{
'id': discount.id,
'name': discount.name,
'total': discount.amount * 100
}
for discount in cart.discounts
],
'total': (cart.subtotal - cart.discount_total) * 100
}
# Build billing address (if available)
billing = None
if has_billing_address():
billing = {
'given_name': billing_addr.first_name,
'family_name': billing_addr.last_name,
'phone': billing_addr.phone,
'address': billing_addr.street,
'city': billing_addr.city,
'state': billing_addr.state,
'country_code': billing_addr.country_code,
'postal_code': billing_addr.postal_code
}
# Build shipping info (if available)
shipping = None
if has_shipping_info():
shipping = {
'address': {
'given_name': shipping_addr.first_name,
'family_name': shipping_addr.last_name,
'phone': shipping_addr.phone,
'address': shipping_addr.street,
'address2': shipping_addr.street2,
'city': shipping_addr.city,
'state': shipping_addr.state,
'country_code': shipping_addr.country_code,
'country': shipping_addr.country,
'postal_code': shipping_addr.postal_code,
'company': shipping_addr.company,
'vat_number': shipping_addr.vat_number,
'notes': shipping_addr.notes
},
'method': {
'name': shipping_method.name,
'provider': shipping_method.provider,
'currency': shipping_method.currency,
'price': shipping_method.cost * 100,
'tax': shipping_method.tax * 100,
'total': (shipping_method.cost + shipping_method.tax) * 100
}
}
# Build customer (if logged in)
customer = None
if is_logged_in():
customer = {
'id': current_user.id,
'email': current_user.email,
'given_name': current_user.first_name,
'family_name': current_user.last_name
# Note: phone field is NOT part of customer object
# Phone is provided in billing/shipping address objects instead
}
# Return response
return {
'purchase': {
'cart': cart,
'billing': billing,
'shipping': shipping,
'customer': customer
}
}
Test your endpoint:
curl -X GET "$MERCHANT_CALLBACKS_BASE/purchase_data" \
-H 'Content-Type: application/json' \
-b 'session=YOUR_SESSION_COOKIE'
| Check | Expected | If it fails |
|---|---|---|
| HTTP status | 200 OK | Check endpoint URL and method (must be GET) |
purchase.cart.items | Array with cart items | Add items to cart before testing |
| All amounts are integers | 89999 not 899.99 | Multiply by 100 (values in cents) |
| Empty cart | {"purchase": null} | This is correct when no cart exists |
purchase.billing | Address object or null | Optional until checkout |
purchase.shipping | Address + method or null | Optional until checkout |
Step 5: Prepare order for webhook confirmation
When (and if) the shopper completes the payment in the Ealyx Pay modal, the Ealyx backend will notify your backend via the POST $MERCHANT_WEBHOOK_BASE/order endpoint (as described in Step 6).
You might need to prepare for this confirmation by creating a pending order on your backend system, and you might not want to do that too early.
The Ealyx SDK will emit an event just before the request is sent to the payment gateway when the shopper completes the payment in the Ealyx Pay modal. For a complete list of all available events, see Available SDK Events.
Below is an example of how to listen for this event and create a pending order.
window.addEventListener("ealyx-ready", () => {
window.Ealyx.addReadyToPlaceOrderListener(() => {
// Create pending order via AJAX
fetch("/orders", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
// All other data will be picked up from the session
body: new URLSearchParams({ payment_method: "ealyx_pay" })
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log("Order created with pending status");
// Ealyx SDK handles redirection
}
});
});
});
Note that this is not mandatory from Ealyx's point of view, as long as you are able to handle the webhook.
Step 6: Handle Order Webhook
As the final step in the Ealyx Pay payment process, the Ealyx backend server will notify your backend server via a server-to-server POST request to the POST $MERCHANT_WEBHOOK_BASE/order endpoint. This is NOT called from the browser/frontend SDK, but directly from Ealyx's servers to your webhook URL.
This endpoint receives payment status updates from Ealyx and must respond with the full order data.
Important: The webhook is called server-to-server by Ealyx infrastructure, not by the frontend SDK. Your webhook URL must be publicly accessible and configured in your Ealyx dashboard.
Understanding IPN (Instant Payment Notification)
The server-to-server notification pattern you're implementing is called an IPN (Instant Payment Notification) — a term commonly used in payment systems like PayPal and Stripe. Ealyx uses the modern term "Webhook" for the same concept.
If you're familiar with PayPal IPNs or Stripe webhooks, the Ealyx webhook works identically:
- Ealyx backend server makes an HTTP POST request to your endpoint
- You validate the request signature (using HMAC-SHA256 with
{USER_ID}-{MERCHANT_ID}) - You process the notification and respond with HTTP 200 OK
This is the core pattern: server-to-server event notification — an essential mechanism in modern payment processing.
Webhook Request:
Ealyx will POST to your webhook endpoint with this format:
POST $MERCHANT_WEBHOOK_BASE/order
X-Ealyx-Signature: hmac_sha256_signature_here
Content-Type: application/json
{
"data": {
"cart_id": "cart_123",
"status": "pending"
}
}
Signature Validation:
The X-Ealyx-Signature header contains an HMAC-SHA256 signature of the request body. The signature is calculated using a secret key derived from your merchant credentials.
Important: The secret key for HMAC is {USER_ID}-{MERCHANT_ID}, where USER_ID and MERCHANT_ID are returned in the initial authentication response. These are not CLIENT_SECRET.
Validate the signature before processing:
# Pseudo-code for signature validation
received_signature = request.headers['X-Ealyx-Signature']
request_body = request.raw_body # Raw bytes, not decoded JSON
# Get USER_ID and MERCHANT_ID from initial auth response
# (stored during initial login - see Authentication section)
user_id = stored_user_id # From auth response
merchant_id = stored_merchant_id # From auth response
secret = user_id + "-" + merchant_id
# Calculate expected signature
expected_signature = HMAC_SHA256(request_body, secret)
# Compare (use constant-time comparison to prevent timing attacks)
if not constant_time_equal(received_signature, expected_signature):
return HTTP 401 Unauthorized
Example (for testing):
# If user_id = "user_12345" and merchant_id = "merchant_67890"
# Then secret = "user_12345-merchant_67890"
BODY='{"data":{"cart_id":"cart_123","status":"pending"}}'
SECRET="user_12345-merchant_67890"
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
Webhook Status Values:
| Status | Meaning | Your Action |
|---|---|---|
processing | ✅ Payment confirmed and ready for fulfillment | ✅ Finalize order, proceed with fulfillment |
pending | Payment received, awaiting confirmation | Accept with 200 OK, keep order in pending state |
payment_review | Payment under review (fraud check, etc.) | Accept with 200 OK, hold order in on_hold state |
canceled | ❌ Payment canceled by customer or failed | ❌ Do not fulfill, mark order as canceled |
✅ Success Status: When you receive
processingstatus, the payment is confirmed and the order is ready for fulfillment. The discount has already been applied by Ealyx. You should proceed with order fulfillment.
Expected Response & HTTP Status:
For all webhook calls, regardless of the status received, you must:
- Always respond with HTTP 200 OK - This tells Ealyx that you received and processed the webhook successfully
- Include the complete order data in the response (same format as your
purchase_dataendpoint)
Ealyx uses the HTTP status code to determine if the webhook was successfully delivered. Even if the payment was canceled or failed, respond with 200 OK to acknowledge receipt.
Your webhook endpoint must respond with:
{
"result": "success",
"message": "Order processed successfully",
"order_ref": "ORD-123456",
"purchase": {
"cart": { ... },
"billing": { ... },
"shipping": { ... },
"customer": { ... }
}
}
Example Implementation:
# Pseudo-code for webhook handler
# 1. Get request data
signature = request.headers['X-Ealyx-Signature']
body = request.raw_body
# 2. Validate signature
# IMPORTANT: The secret is USER_ID-MERCHANT_ID, NOT CLIENT_SECRET
# These values come from the initial auth response
user_id = stored_user_id # From initial auth response
merchant_id = stored_merchant_id # From initial auth response
secret = user_id + "-" + merchant_id
expected_signature = HMAC_SHA256(body, secret)
if not constant_time_equal(signature, expected_signature):
return HTTP 401 Unauthorized
# 3. Parse request
data = json.loads(body)
cart_id = data['data']['cart_id']
status = data['data']['status']
# 4. Find order by cart ID
order = database.find_order_by_cart_id(cart_id)
if not order:
return HTTP 404 Not Found
# 5. Update order status based on webhook status
if status == 'processing':
# ✅ Payment confirmed and ready for fulfillment
# Note: Discount is already applied by Ealyx in the payment gateway
# or will be refunded later (other payment methods)
order.status = 'finalized'
order.process_fulfillment() # Send to warehouse/fulfillment system
order.save()
elif status == 'payment_review':
# Payment under review (fraud check, verification, etc.)
# Hold the order - don't process fulfillment yet
order.status = 'on_hold'
order.save()
elif status == 'pending':
# Payment received but not yet confirmed
# Keep order in pending state, wait for further status updates
order.status = 'pending'
order.save()
elif status == 'canceled':
# ❌ Payment canceled by customer or failed
order.status = 'canceled'
order.save()
# 6. Build response with purchase data
purchase_data = {
'cart': order.get_cart_data(),
'billing': order.get_billing_address(),
'shipping': order.get_shipping_info(),
'customer': order.get_customer_data()
}
# 7. Return response (always 200 OK to acknowledge receipt)
response = {
'result': 'success',
'message': 'Order processed successfully',
'order_ref': order.id,
'purchase': purchase_data
}
return HTTP 200 with JSON response
Testing with curl:
# Create request body
BODY='{"data":{"cart_id":"cart_123","status":"pending"}}'
# Important: The secret is USER_ID-MERCHANT_ID (from auth response), NOT CLIENT_SECRET
# Example: if user_id = "user_12345" and merchant_id = "merchant_67890"
USER_ID="user_12345"
MERCHANT_ID="merchant_67890"
SECRET="$USER_ID-$MERCHANT_ID"
# Calculate signature using the correct secret
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
# Send webhook
curl -X POST '$MERCHANT_WEBHOOK_BASE/order' \
-H "X-Ealyx-Signature: $SIGNATURE" \
-H 'Content-Type: application/json' \
-d "$BODY"
| Check | Expected | If it fails |
|---|---|---|
| Endpoint accessible | HTTP 200 (not 404) | Check URL is publicly accessible from internet |
| Signature validation | Passes | Secret must be USER_ID-MERCHANT_ID, NOT CLIENT_SECRET |
| Response code | HTTP 200 exactly | Ealyx requires 200 (not 201, 204, etc.) |
| Response body | JSON with result, order_ref, purchase | Check response format matches spec |
| Order created | Record in database | Check order creation logic for each status |
Common mistakes:
- Using
CLIENT_SECRETinstead ofUSER_ID-MERCHANT_IDfor signature - Parsing JSON before signature validation (must use raw body bytes)
- Returning 201/204 instead of 200
- Not handling all status values (
processing,pending,payment_review,canceled)
Phase 4: Post-Purchase Updates (Ealyx Pay Only)
Flow Diagram:
If the order is paid with Ealyx Pay, you must keep the order status synchronized with Ealyx throughout its lifecycle (shipment, return, refunds, cart changes). This synchronization is critical for Ealyx to calculate proper disbursements to shoppers.
All order updates use the POST /payments/orders/update endpoint at the Ealyx API.
Update Types:
| Update Type | Use Case | Additional Data Required |
|---|---|---|
shipped | Full order shipment | None |
returned | Full order return/refund | None |
partial_shipment | Some items shipped | List of shipped items |
partial_return | Some items returned | List of returned items |
cart_update | Cart/price changes | Updated cart object |
Full Shipment
When the entire order has been shipped:
curl -X POST '{API_BASE_URL}/payments/orders/update' \
-H 'Authorization: Bearer {ACCESS_TOKEN}' \
-H 'Content-Type: application/json' \
-d '{
"order_id": "ORD-123456",
"update_type": "shipped"
}'
Full Return
When the entire order is returned/refunded:
curl -X POST '{API_BASE_URL}/payments/orders/update' \
-H 'Authorization: Bearer {ACCESS_TOKEN}' \
-H 'Content-Type: application/json' \
-d '{
"order_id": "ORD-123456",
"update_type": "returned"
}'
Partial Shipment
When only some items have been shipped (backorder scenario):
curl -X POST '{API_BASE_URL}/payments/orders/update' \
-H 'Authorization: Bearer {ACCESS_TOKEN}' \
-H 'Content-Type: application/json' \
-d '{
"order_id": "ORD-123456",
"update_type": "partial_shipment",
"shipped_items": [
{
"id": "item_1",
"name": "iPhone 15 Pro",
"description": "Excellent condition",
"url": "https://yoursite.com/products/iphone-15-pro",
"quantity": 1,
"price": 89999,
"tax": 18900,
"total": 108899
},
{
"id": "item_2",
"name": "Phone Case",
"description": "Protective case",
"url": "https://yoursite.com/products/phone-case",
"quantity": 1,
"price": 1999,
"tax": 420,
"total": 2419
}
]
}'
Partial Return
When customer returns only some items:
curl -X POST '{API_BASE_URL}/payments/orders/update' \
-H 'Authorization: Bearer {ACCESS_TOKEN}' \
-H 'Content-Type: application/json' \
-d '{
"order_id": "ORD-123456",
"update_type": "partial_return",
"returned_items": [
{
"id": "item_2",
"name": "Phone Case",
"description": "Protective case",
"url": "https://yoursite.com/products/phone-case",
"quantity": 1,
"price": 1999,
"tax": 420,
"total": 2419
}
]
}'
Cart Update
When order details change after purchase (price adjustment, coupon applied, etc.):
curl -X POST '{API_BASE_URL}/payments/orders/update' \
-H 'Authorization: Bearer {ACCESS_TOKEN}' \
-H 'Content-Type: application/json' \
-d '{
"order_id": "ORD-123456",
"update_type": "cart_update",
"cart": [
{
"id": "cart_123",
"currency": "EUR",
"items": [
{
"id": "item_1",
"name": "iPhone 15 Pro",
"description": "Excellent condition",
"url": "https://yoursite.com/products/iphone-15-pro",
"quantity": 1,
"price": 89999,
"tax": 18900,
"total": 108899
}
],
"price": 89999,
"tax": 18900,
"discounts": [
{
"id": "new_discount",
"name": "Loyalty discount",
"total": 5000
}
],
"total": 84999
}
]
}'
Important Notes:
- All monetary values are in cents (multiply by 100)
- Item structure must match the format from
purchase_dataendpoint - Partial updates must include ALL items/details for that update type
- Send updates as soon as events occur for accurate tracking
- Updates are idempotent - sending same update multiple times is safe
Pseudo-code Implementation:
# When order is shipped
if order.status_changed_to('shipped'):
POST /payments/orders/update:
order_id: order.id
update_type: "shipped"
# When customer returns item
if item.is_returned():
returned_items = get_returned_items()
POST /payments/orders/update:
order_id: order.id
update_type: "partial_return"
returned_items: returned_items
# When price is adjusted (coupon, refund, etc)
if order.total_changed():
new_cart = get_updated_cart()
POST /payments/orders/update:
order_id: order.id
update_type: "cart_update"
cart: [new_cart]