Skip to main content

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.

Prerequisites

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:

  1. show the Ealyx Pay option at checkout,
  2. intercept the form submission to open the Ealyx Modal for payment,
  3. prepare the order for later confirmation (webhook), and
  4. 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.confirmUrl property 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 taxes
  • cart.tax = tax amount only
  • cart.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
}
}
✓ Verification

Test your endpoint:

curl -X GET "$MERCHANT_CALLBACKS_BASE/purchase_data" \
-H 'Content-Type: application/json' \
-b 'session=YOUR_SESSION_COOKIE'
CheckExpectedIf it fails
HTTP status200 OKCheck endpoint URL and method (must be GET)
purchase.cart.itemsArray with cart itemsAdd items to cart before testing
All amounts are integers89999 not 899.99Multiply by 100 (values in cents)
Empty cart{"purchase": null}This is correct when no cart exists
purchase.billingAddress object or nullOptional until checkout
purchase.shippingAddress + method or nullOptional 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:

StatusMeaningYour Action
processing✅ Payment confirmed and ready for fulfillment✅ Finalize order, proceed with fulfillment
pendingPayment received, awaiting confirmationAccept with 200 OK, keep order in pending state
payment_reviewPayment 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 processing status, 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:

  1. Always respond with HTTP 200 OK - This tells Ealyx that you received and processed the webhook successfully
  2. Include the complete order data in the response (same format as your purchase_data endpoint)

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"
✓ Verification
CheckExpectedIf it fails
Endpoint accessibleHTTP 200 (not 404)Check URL is publicly accessible from internet
Signature validationPassesSecret must be USER_ID-MERCHANT_ID, NOT CLIENT_SECRET
Response codeHTTP 200 exactlyEalyx requires 200 (not 201, 204, etc.)
Response bodyJSON with result, order_ref, purchaseCheck response format matches spec
Order createdRecord in databaseCheck order creation logic for each status

Common mistakes:

  • Using CLIENT_SECRET instead of USER_ID-MERCHANT_ID for 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 TypeUse CaseAdditional Data Required
shippedFull order shipmentNone
returnedFull order return/refundNone
partial_shipmentSome items shippedList of shipped items
partial_returnSome items returnedList of returned items
cart_updateCart/price changesUpdated 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_data endpoint
  • 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]