Appendixes
This section contains supplementary materials for testing, deployment, and advanced features. Use these resources after you've completed your basic integration to thoroughly test, optimize, and go live.
Error Handling & Debugging
This section covers common errors, how to identify them, and how to fix them. When something goes wrong with your Ealyx integration, this section will help you debug quickly.
Error Response Format
The Ealyx API returns errors in a consistent JSON format. Understanding this format helps you parse errors programmatically:
{
"error": "invalid_client",
"error_description": "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)"
}
Common error fields:
error- Machine-readable error code (e.g.,invalid_client,invalid_grant,invalid_scope)error_description- Human-readable error message explaining what went wrongstatus- HTTP status code (in the HTTP response, not JSON body)
HTTP Status Codes
| Status | Meaning | Common Cause | Solution |
|---|---|---|---|
| 200 | OK | Request succeeded | Check response body for data |
| 400 | Bad Request | Invalid parameters or malformed request | Check request format, required fields, data types |
| 401 | Unauthorized | Token expired or invalid | Refresh token or re-authenticate |
| 403 | Forbidden | Invalid webhook signature | Check webhook secret format |
| 404 | Not Found | Resource doesn't exist | Verify resource ID (session hash, trade-in ID, order ID) |
| 429 | Too Many Requests | Rate limiting | Implement exponential backoff and retry logic |
| 500 | Server Error | Ealyx API issue | Check status page, contact Ealyx support |
| 503 | Service Unavailable | Maintenance or overload | Retry after a delay |
Error Response Examples by Endpoint
The following are real error responses from actual Ealyx API endpoints. Use these examples to understand what to expect when things go wrong:
OAuth Token Endpoint Errors (POST /oauth2/token/)
Invalid Credentials (400)
{
"error": "invalid_grant",
"error_description": "Invalid username or password"
}
Missing Scope (400)
{
"error": "invalid_scope",
"error_description": "The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner"
}
Token Expired (401)
{
"error": "invalid_grant",
"error_description": "The provided refresh token has expired"
}
Shopper Data Endpoint Errors (GET {MERCHANT_CALLBACKS_BASE}/shopper_data)
Customer Not Found (404)
{
"error": "not_found",
"error_description": "Customer with ID 'cust_12345' not found in database"
}
Trade-in Item Errors (GET /core/tradeinitems/{trade_in_id})
Invalid Trade-in ID (404)
{
"error": "not_found",
"error_description": "Trade-in valuation with ID 'tradein_invalid' not found"
}
Valuation Expired (410)
{
"error": "gone",
"error_description": "This trade-in valuation expired at 2024-12-15T18:30:00Z"
}
Session Lookup Errors (GET /core/sessions/{session_hash})
Missing Authorization (401)
{
"error": "unauthorized",
"error_description": "Authorization header required. Format: Authorization: Bearer {ACCESS_TOKEN}"
}
Session Not Found (404)
{
"error": "not_found",
"error_description": "Session with hash 'abc123def456' not found or has expired"
}
Session Invalidated - Use New Version (410)
{
"error": "gone",
"error_description": "This session version has been superseded. Use the new session_hash returned in the response."
}
Response with Session Update (200 with new hash)
{
"session_id": "abc123def456|002",
"tradein_item": {...},
"note": "Session was updated from 'abc123def456|001' to 'abc123def456|002' due to cart changes"
}
When you receive a different session_id than the one you queried:
- Save the new
session_hash(regenerate from the newsession_id) - Re-query the endpoint with the updated hash
- Use the returned trade-in data for your validation logic
Register Purchase Errors (POST /register-purchase)
Missing Authorization (401)
{
"error": "unauthorized",
"error_description": "Authorization header required. Format: Authorization: Bearer {ACCESS_TOKEN}"
}
Invalid Session Hash (404)
{
"error": "not_found",
"error_description": "Session with hash 'abc123def456' not found or has expired"
}
Invalid Cart Format (400)
{
"error": "bad_request",
"error_description": "Cart parameter is not valid Base64 or malformed JSON. Expected: Base64(JSON)"
}
Monetary Values Invalid (400)
{
"error": "bad_request",
"error_description": "order_amount must be integer in cents. Received: '89.99' (float). Expected: '8999' (integer)"
}
Order Update Errors (POST /payments/orders/update)
Wrong Token Scope (403)
{
"error": "forbidden",
"error_description": "Token has scope 'core payments valuations' but endpoint requires scope 'updates'"
}
Order Not Found (404)
{
"error": "not_found",
"error_description": "Order with ID 'ORD-999' not found in Ealyx system"
}
Invalid Update Type (400)
{
"error": "bad_request",
"error_description": "update_type 'invalid_status' is not allowed. Valid values: 'shipped', 'delivered', 'cancelled'"
}
Server Errors
Rate Limit Exceeded (429)
{
"error": "rate_limit_exceeded",
"error_description": "You have exceeded the rate limit of 100 requests per minute"
}
Server Error (500)
{
"error": "internal_server_error",
"error_description": "An internal server error occurred. Correlation ID: abc123def456 (include in support request)"
}
Common Errors & Solutions
Authentication Errors
Error: invalid_client
{
"error": "invalid_client",
"error_description": "Client authentication failed"
}
Causes:
- Typo in CLIENT_ID or CLIENT_SECRET
- Wrong BASE64 encoding of credentials
- Credentials not separated by colon in Basic auth
Solution:
# Verify Base64 encoding is correct
echo -n "CLIENT_ID:CLIENT_SECRET" | base64
# Should produce: Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=
# Test with curl
curl -X POST '{API_BASE_URL}/oauth2/token/' \
-H "Authorization: Basic $(echo -n 'your_client_id:your_client_secret' | base64)" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=password' \
-d 'username=your_username' \
-d 'password=your_password' \
-d 'scope=core payments valuations'
Error: 401 Unauthorized on API requests
Causes:
- Token expired (default 1 hour)
- Token never refreshed
- Wrong token type (not Bearer)
Solution:
# Check if token is expired
# Tokens expire 1 hour after issuing or refreshing
# Store expires_at in database and check before each request
# If expired, refresh the token
curl -X POST '{API_BASE_URL}/oauth2/token/' \
-H "Authorization: Basic $(echo -n 'your_client_id:your_client_secret' | base64)" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=refresh_token' \
-d 'refresh_token=your_stored_refresh_token'
# If refresh fails, re-authenticate with credentials
Order Webhook Errors
Error: 403 Forbidden on order webhook endpoint
Causes:
- Webhook signature validation failed
- Wrong signature secret (using CLIENT_SECRET instead of USER_ID-MERCHANT_ID)
Solution:
# 1. Verify signature secret format
# Secret MUST be: "{USER_ID}-{MERCHANT_ID}"
# NOT CLIENT_SECRET
# 2. Test signature validation with known values
USER_ID="user_12345"
MERCHANT_ID="merchant_67890"
BODY='{"data":{"cart_id":"cart_123","status":"pending"}}'
SECRET="$USER_ID-$MERCHANT_ID"
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
echo "Expected signature: $SIGNATURE"
# Compare with received X-Ealyx-Signature header
Error: Webhook received but signature validation fails
Causes:
- Using parsed JSON instead of raw body
- Wrong secret (CLIENT_SECRET instead of USER_ID-MERCHANT_ID)
- Body modified between receipt and validation
- Framework not providing raw body correctly
Solution:
# Use raw request body (before JSON parsing)
# Different frameworks handle this differently:
# Node.js/Express:
# const rawBody = req.rawBody; // or req.body (need express.raw() middleware)
# Laravel/PHP:
# $rawBody = file_get_contents('php://input');
# Python/Flask:
# raw_body = request.get_data()
# Always validate using the raw bytes received
received_signature="from_X_Ealyx_Signature_header"
raw_body="raw_bytes_received"
secret="$USER_ID-$MERCHANT_ID"
expected_signature=$(echo -n "$raw_body" | openssl dgst -sha256 -hmac "$secret" | cut -d' ' -f2)
if [ "$received_signature" != "$expected_signature" ]; then
echo "✗ Signature validation failed"
# Log both signatures for debugging
echo "Received: $received_signature"
echo "Expected: $expected_signature"
fi
Data Format Errors
Error: API rejects request with 400 Bad Request
Causes (by endpoint):
- Monetary values as decimals instead of cents (€99.99 should be 9999, not 99.99)
- Missing required fields in cart, billing, shipping objects
- Wrong data types (string instead of number, boolean instead of string)
- Cart data for register-purchase not properly Base64 encoded
- Request body for register-purchase not properly formatted as JSON
Solution:
# 1. Verify monetary values are integers in cents
# ✗ Wrong: "price": 99.99
# ✓ Correct: "price": 9999
# 2. Verify cart structure for purchase_data
curl -X GET '{MERCHANT_CALLBACKS_BASE}/purchase_data' \
-H 'Content-Type: application/json'
# Response must include: cart.id, cart.currency, cart.items[], cart.price, cart.tax, cart.total
# 3. Verify Base64 encoding for register-purchase cart parameter
CART_JSON='{"id":"cart_123","currency":"EUR",...}'
CART_B64=$(echo -n "$CART_JSON" | base64)
echo "Encoded: $CART_B64"
# Use in request body:
curl -X POST "{API_BASE_URL}/register-purchase" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cart":"'$CART_B64'","order_amount":89999}'
Debugging Checklist
Before contacting Ealyx support, verify these items:
Authentication Issues:
- CLIENT_ID and CLIENT_SECRET are correct
- Base64 encoding of credentials is correct
- Token has been saved to database after authentication
- Current time is before token expiration time
- If token refresh failed, tried re-authentication
API Call Issues:
- All monetary values are in cents (integers)
- All required fields are present in request body
- Data types match specification (strings, numbers, booleans)
- Authorization header format is correct:
Bearer {ACCESS_TOKEN} - For POST requests, JSON body is properly formatted and contains all required fields
Webhook Issues:
- Webhook URL is publicly accessible (test with curl from different machine)
- Webhook endpoint returns HTTP 200 on success
- Signature validation secret is
{USER_ID}-{MERCHANT_ID}, NOT CLIENT_SECRET - Raw request body is used for signature validation (not parsed JSON)
- Framework correctly provides raw body before parsing
- Received signature and calculated signature match exactly
Session/Trade-in Issues:
- Session hash is generated consistently from same session ID
- Session hash matches what's sent from frontend
- Trade-in valuation hasn't expired (check valid_until timestamp)
- Trade-in is in "accepted" status before attempting purchase registration
Logging Best Practices
Enable detailed logging to help with debugging:
# Log every API request/response
echo "[$(date)] REQUEST: POST $API_BASE_URL/oauth2/token/" >> ealyx.log
echo "[$(date)] AUTH_RESPONSE: $AUTH_RESPONSE" >> ealyx.log
# Log token operations
echo "[$(date)] TOKEN_REFRESHED: New token expires at $(date -d '+1 hour')" >> ealyx.log
# Log webhook operations
echo "[$(date)] WEBHOOK_RECEIVED: X-Ealyx-Signature=$RECEIVED_SIG" >> ealyx.log
echo "[$(date)] CALCULATED_SIGNATURE: $EXPECTED_SIG" >> ealyx.log
echo "[$(date)] SIGNATURE_VALID: $([ "$RECEIVED_SIG" = "$EXPECTED_SIG" ] && echo 'YES' || echo 'NO')" >> ealyx.log
# Log errors with full context
echo "[$(date)] ERROR: $ERROR_CODE - $ERROR_MESSAGE" >> ealyx.log
echo "[$(date)] REQUEST_BODY: $REQUEST_BODY" >> ealyx.log
echo "[$(date)] RESPONSE_CODE: $HTTP_STATUS" >> ealyx.log
echo "[$(date)] RESPONSE_BODY: $RESPONSE_BODY" >> ealyx.log
Edge Cases & Common Gotchas
Session hash changes:
- If a customer visits your site multiple times, the session ID might change
- Always generate the session hash dynamically from the current session ID, not cached
- If Ealyx returns a different
session_idin responses, that's normal (session redirect)
Token refresh fails:
- If token refresh fails, don't immediately fail - try re-authenticating with credentials
- Store both CLIENT_SECRET and credentials so you can re-auth if needed
- Implement exponential backoff for retries (wait 1s, then 2s, then 4s, etc.)
Valuation expires:
- Trade-in valuations have an expiration time (
valid_untilfield) - If customer doesn't complete purchase within 24-48 hours, they need to re-value
- Always check the
valid_untiltimestamp before processing purchase registration
Webhook arrives before order is created:
- There's a race condition: webhook might arrive before your order creation completes
- Solution: Create order with
pendingstatus immediately when payment starts - Webhook will update the status, not create the order
Cart changes between checkout and payment:
- Customer might modify their cart after opening Ealyx Pay modal
- This is an edge case - either lock the cart or handle price mismatches gracefully
- The
purchase_dataendpoint is called fresh when modal opens, so it reflects current cart
Duplicate order prevention:
- If webhook times out and retries, you might create the same order twice
- Solution: Use idempotency - check if order with same
cart_idalready exists - Implement database constraint on
(cart_id, status)to prevent duplicates
Signature validation with different encoding:
- Different languages encode HMAC differently (hex vs base64 vs raw bytes)
- Always use hex encoding:
openssl dgst -sha256 -hmac - Test with known values first before going live
Cart data encoding:
- When using register-purchase endpoint, cart JSON is Base64-encoded before being included in request body
- Base64 encoding increases data size (~33% larger), but request body has no size limits
Duplicate transaction attempts:
- Customer might click "Pay" twice if interface is slow
- Your webhook endpoint should be idempotent (receiving same webhook twice shouldn't create duplicate refunds)
- Use unique identifiers (order_id, cart_id) to detect and skip duplicates
Security Deep Dive
This section provides detailed explanations of critical security concepts used in Ealyx integration. Understanding these concepts will help you implement secure webhooks and session management.
Webhook Signature Validation - Timing Attack Prevention
The Problem: Timing Attacks
A naive signature comparison using string equality (==) is vulnerable to timing attacks:
# ❌ VULNERABLE - Time varies based on first mismatch position
if [ "$received_sig" = "$calculated_sig" ]; then
echo "Valid"
else
echo "Invalid"
fi
Why? Because in most languages, string comparison stops at the first mismatched character. An attacker can:
- Send invalid signatures with different first characters
- Measure response time for each attempt
- Deduce the correct signature character by character
- Forge valid signatures by exploiting timing differences
The Solution: Constant-Time Comparison
Always compare signatures using constant-time comparison that takes the same time regardless of where the mismatch is:
#!/bin/bash
# Function: Constant-time string comparison
# Returns 0 (success) if strings match, 1 if they don't
# Always compares all characters regardless of mismatches
constant_time_compare() {
local expected="$1"
local received="$2"
local exp_len=${#expected}
local rec_len=${#received}
local result=0
# Compare lengths first (must match)
if [ "$exp_len" -ne "$rec_len" ]; then
return 1
fi
# Compare character by character (always iterate all)
for ((i = 0; i < exp_len; i++)); do
if [ "${expected:$i:1}" != "${received:$i:1}" ]; then
result=1
fi
done
return $result
}
# Usage:
RECEIVED_SIG="abc123def456" # From webhook header
CALCULATED_SIG="abc123def456" # Computed from body + secret
if constant_time_compare "$CALCULATED_SIG" "$RECEIVED_SIG"; then
echo "✓ Signature is valid"
else
echo "✗ Signature is invalid"
fi
Framework-Specific Implementations:
If you're using a framework, use its built-in constant-time comparison:
# PHP 5.6+
hash_equals($expected, $received)
# Node.js / TypeScript
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
# Python 3
hmac.compare_digest(expected, received)
# Go
subtle.ConstantTimeCompare([]byte(expected), []byte(received))
# Ruby
ActiveSupport::SecurityUtils.secure_compare(expected, received)
Test Cases for Signature Validation:
Use these test cases to verify your implementation:
#!/bin/bash
# Test your signature validation against known values
USER_ID="123456"
MERCHANT_ID="789012"
SECRET="$USER_ID-$MERCHANT_ID"
# Test body (raw JSON string as it comes from Ealyx)
BODY='{"order_id":"order_123","status":"completed","cart_id":"cart_456"}'
# Calculate expected signature
EXPECTED=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | cut -d' ' -f2)
echo "Expected signature: $EXPECTED"
# Test cases
test_signature() {
local received="$1"
local description="$2"
if constant_time_compare "$EXPECTED" "$received"; then
echo "✓ PASS: $description"
else
echo "✗ FAIL: $description"
fi
}
# Run tests
test_signature "$EXPECTED" "Exact match (should pass)"
test_signature "invalid_signature_here" "Invalid signature (should fail)"
test_signature "${EXPECTED:0:10}invalid" "Partial match (should fail)"
test_signature "$EXPECTED " "Trailing space (should fail)"
Same-Origin Requests - No CORS Required
Why CORS is Not Needed
The Ealyx frontend SDK is injected into your website and executes in the same domain context as your web application. This means:
- ✅ The SDK runs at
https://yoursite.com(not from Ealyx servers) - ✅ Calls to
shopper_dataandpurchase_dataendpoints are same-origin requests - ✅ No CORS headers are required
How It Works
Your website: https://yoursite.com
SDK injected in: https://yoursite.com (same domain)
Callback URL: {MERCHANT_CALLBACKS_BASE}/shopper_data
✅ Same origin → No CORS validation needed
What This Means for Your Implementation
Your callback endpoints (shopper_data, purchase_data) are regular same-origin requests, just like any other API call within your application. You can handle them exactly as you would any internal endpoint:
# Example: Simple same-origin request
curl -X GET '{MERCHANT_CALLBACKS_BASE}/shopper_data' \
-H 'Content-Type: application/json' \
-b 'Cookie-Session: $SESSION'
No special CORS headers, Origin validation, or OPTIONS handling is required.
Session Hash Security - Protecting Session IDs
The Problem: Session ID Exposure
If you send your session ID directly to Ealyx, an attacker who intercepts it could:
- Use the same session ID for a different customer
- Access another customer's trade-in valuation
- Complete someone else's purchase
The Solution: Session Hash with Salt
Always hash your session ID with HMAC-SHA256 before sending it to Ealyx:
#!/bin/bash
# Generate a secure session ID (your framework usually does this)
SESSION_ID=$(php -r 'echo bin2hex(random_bytes(32));')
# Generate a random salt to use as HMAC key
SALT=$(php -r 'echo bin2hex(random_bytes(16));')
# Use HMAC-SHA256 with salt as the key (NOT simple concatenation)
SESSION_HASH=$(echo -n "$SESSION_ID" | openssl dgst -sha256 -hmac "$SALT" | cut -d' ' -f2)
# Send SESSION_HASH to Ealyx, keep SESSION_ID and SALT in your session storage
echo "Session ID (keep secret): $SESSION_ID"
echo "Session Salt (keep secret): $SALT"
echo "Session Hash (send to Ealyx): $SESSION_HASH"
Why HMAC-SHA256 Instead of Simple Hash?
- Timing-attack resistant: Takes same time regardless of where comparison fails
- Keyed hash: Salt acts as HMAC secret, not just concatenated data
- Industry standard: Used by all major frameworks and APIs
- More secure: Better prevents hash collision attacks
Additional Security Options:
You can enhance the hash generation with:
# Option 1: Include timestamp for time-limited sessions
SESSION_ID="your_session_id" # From your session storage
SALT="random_salt_value" # Generated when creating session
HASH_INPUT="${SESSION_ID}:$(date +%s)"
SESSION_HASH=$(echo -n "$HASH_INPUT" | openssl dgst -sha256 -hmac "$SALT" | cut -d' ' -f2)
# Option 2: Include customer ID for uniqueness
CUSTOMER_ID="customer_12345" # Your internal customer ID
HASH_INPUT="${SESSION_ID}:${CUSTOMER_ID}"
SESSION_HASH=$(echo -n "$HASH_INPUT" | openssl dgst -sha256 -hmac "$SALT" | cut -d' ' -f2)
Storing Salt for Verification:
You'll need to verify the session hash when Ealyx sends it back:
# Store this in your session when generated
SESSION_ID="abc123def456" # Your session identifier
SALT="random_salt_xyz" # Salt generated with session
SESSION_HASH="hash_value_here" # HMAC-SHA256 result
SESSION_DATA="{
'session_id': '$SESSION_ID',
'salt': '$SALT',
'hash': '$SESSION_HASH',
'created_at': '$(date -u +%Y-%m-%dT%H:%M:%SZ)'
}"
# Save to database or session storage
# Then when you receive a hash from Ealyx, regenerate it and compare
REGENERATED_HASH=$(echo -n "${SESSION_ID}${SALT}" | sha256sum | cut -d' ' -f1)
if [ "$REGENERATED_HASH" = "$RECEIVED_HASH" ]; then
echo "✓ Session hash is valid"
else
echo "✗ Session hash does not match"
fi
Testing Guide
Before deploying to production, thoroughly test each component of your integration. This section provides concrete testing strategies, test data, and scripts for verifying your implementation at each stage.
Test Environment Setup
Your Ealyx sandbox account provides a complete testing environment with:
- Test API credentials that work without real charges
- Webhook event simulation
- Multiple merchant configurations to test different scenarios
- Test data that doesn't affect production
Testing Individual Endpoints
Testing Authentication (POST /oauth2/token/)
What to test:
- Successful token generation
- Token expiration and refresh
- Invalid credentials handling
- Concurrent token requests
Test script:
#!/bin/bash
# test-auth.sh - Test authentication endpoints
source ealyx-token-manager.sh
source ealyx-config.sh
echo "=== Testing Authentication ==="
# Test 1: Valid credentials
echo "Test 1: Valid credentials"
RESPONSE=$(curl -s -X POST '{API_BASE_URL}/oauth2/token/' \
-H "Authorization: Basic $(echo -n "$CLIENT_ID:$CLIENT_SECRET" | base64)" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=password' \
-d "username=$USERNAME" \
-d "password=$PASSWORD" \
-d 'scope=core payments valuations')
ACCESS_TOKEN=$(echo $RESPONSE | jq -r '.access_token // "null"')
if [ "$ACCESS_TOKEN" != "null" ] && [ ! -z "$ACCESS_TOKEN" ]; then
echo "✓ PASS: Token generated successfully"
echo " Token (first 20 chars): ${ACCESS_TOKEN:0:20}..."
else
echo "✗ FAIL: No access token received"
echo " Response: $RESPONSE"
exit 1
fi
# Test 2: Token contains required claims
echo ""
echo "Test 2: Token structure"
CLAIMS=$(echo $ACCESS_TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq '.' 2>/dev/null)
if echo "$CLAIMS" | grep -q "exp"; then
echo "✓ PASS: Token contains expiration claim"
else
echo "✗ FAIL: Token structure invalid"
exit 1
fi
# Test 3: Invalid credentials
echo ""
echo "Test 3: Invalid credentials"
INVALID_RESPONSE=$(curl -s -X POST '{API_BASE_URL}/oauth2/token/' \
-H "Authorization: Basic $(echo -n "invalid:invalid" | base64)" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=password' \
-d 'username=test' \
-d 'password=wrong' \
-d 'scope=core')
ERROR=$(echo $INVALID_RESPONSE | jq -r '.error // "null"')
if [ "$ERROR" = "invalid_client" ] || [ "$ERROR" = "invalid_grant" ]; then
echo "✓ PASS: Invalid credentials rejected correctly"
echo " Error: $ERROR"
else
echo "✗ FAIL: Expected error response"
echo " Response: $INVALID_RESPONSE"
fi
echo ""
echo "=== Authentication tests complete ==="
Testing shopper_data Endpoint
What to test:
- Successful data retrieval
- Missing customer data handling
Test script:
#!/bin/bash
# test-shopper-data.sh - Test shopper_data endpoint
source ealyx-token-manager.sh
source ealyx-config.sh
echo "=== Testing shopper_data Endpoint ==="
# Get valid token
ealyx_authenticate "$USERNAME" "$PASSWORD"
ACCESS_TOKEN=$(ealyx_get_token)
MERCHANT_ID=$(grep "MERCHANT_ID=" /tmp/ealyx_tokens.env | cut -d'=' -f2)
USER_ID=$(grep "USER_ID=" /tmp/ealyx_tokens.env | cut -d'=' -f2)
# Test 1: Valid shopper_data request
echo "Test 1: Valid shopper_data request"
SHOPPER_DATA=$(cat <<'EOF'
{
"customer": {
"id": "cust-123",
"email": "test@example.com",
"given_name": "John",
"family_name": "Doe"
},
"merchant_id": "$MERCHANT_ID"
}
EOF
)
RESPONSE=$(curl -s -X POST '{API_BASE_URL}/shopper-data/' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d "$SHOPPER_DATA")
SESSION_HASH=$(echo $RESPONSE | jq -r '.session_hash // "null"')
if [ "$SESSION_HASH" != "null" ]; then
echo "✓ PASS: Session hash generated"
echo " Session hash: $SESSION_HASH"
else
echo "✗ FAIL: No session hash in response"
echo " Response: $RESPONSE"
fi
echo ""
echo "=== shopper_data tests complete ==="
Testing purchase_data Endpoint
What to test:
- Cart data validation
- Pricing calculations (ensure cents format)
- Discount application
- Missing payment method handling
Test script:
#!/bin/bash
# test-purchase-data.sh - Test purchase_data endpoint
source ealyx-token-manager.sh
source ealyx-config.sh
echo "=== Testing purchase_data Endpoint ==="
ACCESS_TOKEN=$(ealyx_get_token)
MERCHANT_ID=$(grep "MERCHANT_ID=" /tmp/ealyx_tokens.env | cut -d'=' -f2)
USER_ID=$(grep "USER_ID=" /tmp/ealyx_tokens.env | cut -d'=' -f2)
# Test 1: Valid purchase_data with prices in cents
echo "Test 1: Valid purchase_data request (prices in cents)"
PURCHASE_DATA=$(cat <<'EOF'
{
"customer": {
"id": "cust-123",
"email": "test@example.com",
"given_name": "John",
"family_name": "Doe"
},
"cart": {
"items": [
{
"id": "item-1",
"name": "Test Product",
"price": 9999,
"quantity": 1
}
],
"subtotal": 9999,
"tax": 1920,
"total": 11919
},
"payment_method": "ealyx_pay",
"session_hash": "test-hash-123"
}
EOF
)
RESPONSE=$(curl -s -X GET '{MERCHANT_CALLBACKS_BASE}/purchase_data' \
-H 'Content-Type: application/json')
STATUS=$(echo $RESPONSE | jq -r '.status // "null"')
if [ "$STATUS" != "null" ]; then
echo "✓ PASS: Purchase data accepted"
echo " Response status: $STATUS"
else
echo "✗ FAIL: Purchase data rejected"
echo " Response: $RESPONSE"
fi
# Test 2: Invalid prices (decimal instead of cents)
echo ""
echo "Test 2: Invalid purchase_data (prices as decimals)"
INVALID_PURCHASE=$(cat <<'EOF'
{
"customer": {
"id": "cust-456",
"email": "test2@example.com",
"given_name": "Jane",
"family_name": "Smith"
},
"cart": {
"items": [
{
"id": "item-2",
"name": "Test Product 2",
"price": 99.99,
"quantity": 1
}
],
"subtotal": 99.99,
"tax": 19.20,
"total": 119.19
},
"payment_method": "ealyx_pay"
}
EOF
)
ERROR_RESPONSE=$(curl -s -X GET '{MERCHANT_CALLBACKS_BASE}/purchase_data' \
-H 'Content-Type: application/json')
ERROR=$(echo $ERROR_RESPONSE | jq -r '.error // "null"')
if [ "$ERROR" != "null" ]; then
echo "✓ PASS: Decimal prices correctly rejected"
echo " Error: $ERROR"
else
echo "⚠ WARNING: Expected validation error for decimal prices"
fi
echo ""
echo "=== purchase_data tests complete ==="
Testing Full Flows
Discovery → Valuation → Payment Flow
What to test:
- Teaser placement and SDK initialization
- Valuation modal opens and accepts offers
- Payment method selection
- Order creation after successful payment
Manual testing checklist:
Discovery Phase:
☐ Teasers appear on home page
☐ Teasers appear on product page
☐ Teasers appear on cart page
☐ Teasers appear on checkout page
☐ Clicking teaser opens valuation modal
Valuation Phase:
☐ Category and product selection works
☐ Condition questions display correctly
☐ Offer is generated without errors
☐ Accept offer button enables payment
☐ Offer displays in subsequent pages
Payment Phase (Ealyx Pay):
☐ Ealyx Pay option appears at checkout
☐ Clicking Ealyx Pay opens payment modal
☐ Enter test payment credentials: 4242424242424242
☐ Test expiry: any future date
☐ Test CVC: any 3 digits
☐ Payment completes without errors
☐ Order is created with trade-in discount applied
☐ Webhook is received within 5 seconds
Post-Purchase:
☐ Order confirmation shows trade-in discount
☐ Customer receives confirmation email
☐ Trade-in status is tracked in admin
☐ Order history shows original and final price
Testing Order Webhook (POST /order)
Order Webhook Reception Testing
Local webhook testing with ngrok:
#!/bin/bash
# test-webhook-locally.sh - Test webhook reception locally
echo "=== Local Webhook Testing Setup ==="
# 1. Start ngrok tunnel (requires ngrok installed)
echo "Starting ngrok tunnel..."
ngrok http 3000 --subdomain=your-company-test &
NGROK_PID=$!
# Wait for ngrok to start
sleep 3
# 2. Get the public URL
PUBLIC_URL=$(curl -s http://localhost:4040/api/tunnels | jq -r '.tunnels[0].public_url')
echo "Public URL: $PUBLIC_URL"
# 3. Update your webhook URL in Ealyx dashboard to:
# $PUBLIC_URL/ealyx/webhooks
# 4. Create a simple webhook receiver
cat > webhook-receiver.js << 'NODEJS'
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/ealyx/webhooks', (req, res) => {
const signature = req.headers['x-ealyx-signature'];
const secret = process.env.WEBHOOK_SECRET; // USER_ID-MERCHANT_ID
// Calculate expected signature
const hmac = crypto.createHmac('sha256', secret);
hmac.update(req.body);
const expected = hmac.digest('hex');
// Constant-time comparison
const valid = crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
if (!valid) {
console.error('Invalid signature:', signature);
return res.status(401).send('Invalid signature');
}
// Log the webhook
const event = JSON.parse(req.body);
console.log('✓ Webhook received:', event);
// Store in database or process
// db.saveWebhook(event);
res.status(200).send('OK');
});
app.listen(3000, () => {
console.log('Webhook receiver listening on port 3000');
});
NODEJS
# 5. Run the receiver
echo "Starting webhook receiver..."
WEBHOOK_SECRET="your-user-id-your-merchant-id" node webhook-receiver.js
# Cleanup on exit
trap "kill $NGROK_PID" EXIT
Webhook Signature Validation Testing
Test that your signature validation is correct:
#!/bin/bash
# test-webhook-signature.sh - Verify signature validation logic
source ealyx-token-manager.sh
source ealyx-config.sh
echo "=== Testing Webhook Signature Validation ==="
# Read credentials from token file
USER_ID=$(grep "USER_ID=" /tmp/ealyx_tokens.env | cut -d'=' -f2)
MERCHANT_ID=$(grep "MERCHANT_ID=" /tmp/ealyx_tokens.env | cut -d'=' -f2)
SECRET="$USER_ID-$MERCHANT_ID"
# Test webhook payload
WEBHOOK_PAYLOAD='{"order_id":"order-123","status":"completed","trade_in_discount":5000}'
# Calculate correct signature
SIGNATURE=$(echo -n "$WEBHOOK_PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
echo "Payload: $WEBHOOK_PAYLOAD"
echo "Secret: $SECRET"
echo "Signature: $SIGNATURE"
echo ""
# Test 1: Correct signature
echo "Test 1: Validating correct signature"
VALIDATION=$(ealyx_validate_signature "$WEBHOOK_PAYLOAD" "$SIGNATURE" "$SECRET")
if [ $? -eq 0 ]; then
echo "✓ PASS: Signature validated successfully"
else
echo "✗ FAIL: Valid signature rejected"
fi
# Test 2: Invalid signature
echo ""
echo "Test 2: Rejecting invalid signature"
INVALID_SIGNATURE="0000000000000000000000000000000000000000000000000000000000000000"
VALIDATION=$(ealyx_validate_signature "$WEBHOOK_PAYLOAD" "$INVALID_SIGNATURE" "$SECRET" 2>/dev/null)
if [ $? -ne 0 ]; then
echo "✓ PASS: Invalid signature rejected correctly"
else
echo "✗ FAIL: Invalid signature was not rejected"
fi
# Test 3: Tampered payload
echo ""
echo "Test 3: Rejecting tampered payload"
TAMPERED_PAYLOAD='{"order_id":"order-456","status":"completed","trade_in_discount":50000}'
VALIDATION=$(ealyx_validate_signature "$TAMPERED_PAYLOAD" "$SIGNATURE" "$SECRET" 2>/dev/null)
if [ $? -ne 0 ]; then
echo "✓ PASS: Tampered payload rejected correctly"
else
echo "✗ FAIL: Tampered payload was not detected"
fi
echo ""
echo "=== Webhook signature tests complete ==="
Test Data Reference
Valid Test Credentials for Sandbox
# Test user (all merchant operations)
USERNAME="test-user"
PASSWORD="test-password"
# Test payment method
CARD_NUMBER="4242424242424242"
CARD_EXPIRY="12/25"
CARD_CVC="123"
# Test merchant accounts (use in requests)
MERCHANT_ID="test-merchant-001"
USER_ID="test-user-001"
# Test customer data
CUSTOMER_ID="cust-test-001"
CUSTOMER_EMAIL="test@example.com"
CUSTOMER_NAME="Test User"
# Test cart data (prices in CENTS)
PRODUCT_PRICE_CENTS=9999 # €99.99
PRODUCT_TAX_CENTS=1920 # €19.20 (VAT)
PRODUCT_TOTAL_CENTS=11919 # €119.19
Test Trade-In Scenarios
| Scenario | Category | Product | Condition | Expected Discount |
|---|---|---|---|---|
| High-value device | Smartphone | iPhone 15 Pro | Excellent | €300-400 |
| Medium-value device | Smartphone | iPhone 12 | Good | €150-200 |
| Low-value device | Smartphone | iPhone XR | Fair | €50-100 |
| Damaged device | Smartphone | iPhone 11 | Poor | €20-50 |
| Non-trade-in item | Other | Accessory | N/A | €0 |
Testing Payment Method Paths
# Test 1: Ealyx Pay path
# - select "ealyx_pay" in purchase_data
# - customer enters credit card
# - payment completes in modal
# - webhook received
# Test 2: Other payment methods path
# - select "credit_card" / "bank_transfer" / etc.
# - payment processed through external method
# - confirmation received
# - manual order registration via API
# Test 3: Mixed scenario
# - Ealyx Pay is DECLINED
# - Fall back to other payment method
# - Register purchase with alternative method
# - Ensure discount is still applied
Common Testing Issues & Solutions
Issue: Webhook not received
# Debugging steps:
1. Check ngrok tunnel is active
2. Verify webhook URL in Ealyx dashboard matches ngrok URL
3. Check firewall allows inbound requests to localhost:3000
4. Enable webhook event logging
5. Check server logs for incoming requests
# Quick test:
curl -X POST https://your-ngrok-url.ngrok.io/ealyx/webhooks \
-H 'Content-Type: application/json' \
-H 'X-Ealyx-Signature: test' \
-d '{"test":"event"}'
Issue: Token expires during testing
# Use the token manager script to auto-refresh:
source ealyx-token-manager.sh
# This will automatically refresh if expired:
ealyx_api POST /endpoint '{"data":"value"}'
# Or manually refresh:
ealyx_refresh_token
Issue: Signature validation failing
# Common causes:
1. Using CLIENT_SECRET instead of USER_ID-MERCHANT_ID
2. Using parsed JSON instead of raw request body
3. Wrong hash algorithm (should be HMAC-SHA256)
4. Timing comparison issue (use constant-time comparison)
# Debug by logging:
echo "Raw body bytes: $(cat request.body | xxd)"
echo "Signature from header: $sig_from_header"
echo "Calculated signature: $calculated_sig"
Advanced Features
This section covers optional features and advanced configurations for integrators who want more control or flexibility beyond the basic implementation covered in the Shopper Journey section.
Token Management Strategies
Multiple Token Management Strategies
Depending on your infrastructure, you might want different token storage approaches:
Strategy 1: Database with Expiration Tracking (Recommended for Production)
# Schema
CREATE TABLE ealyx_tokens (
merchant_id VARCHAR(255) PRIMARY KEY,
access_token VARCHAR(255) NOT NULL,
refresh_token VARCHAR(255) NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
# Function to get valid token
get_valid_token() {
local now=$(date +%s)
local expires_at=$(mysql -e "SELECT expires_at FROM ealyx_tokens LIMIT 1" | tail -1)
local buffer=300 # Refresh 5 minutes before expiry
if [ $((expires_at - now)) -lt $buffer ]; then
refresh_token
fi
echo $(mysql -e "SELECT access_token FROM ealyx_tokens LIMIT 1" | tail -1)
}
Strategy 2: File-Based with Lock (Simple Development)
# Store tokens in encrypted file
TOKEN_FILE="/tmp/ealyx_tokens.json"
TOKEN_LOCK="/tmp/ealyx_tokens.lock"
# Atomic read/write with file locking
get_token() {
exec 200>"$TOKEN_LOCK"
flock 200
cat "$TOKEN_FILE" | jq -r '.access_token'
flock -u 200
}
Widget Visualization
Basic Display Teasers
Add teaser placeholders across your site:
<!-- Homepage banner -->
<div class="ealyx-teaser" data-type="banner"></div>
<!-- Product detail page -->
<div class="ealyx-teaser" data-type="product-widget"></div>
<!-- Cart page (both types) -->
<div class="ealyx-teaser" data-type="product-widget"></div>
<div class="ealyx-teaser" data-type="cart-summary"></div>
<!-- Checkout page -->
<div class="ealyx-teaser" data-type="cart-summary"></div>
<!-- Thank you page -->
<div class="ealyx-teaser" data-type="thank-you"></div>
Alternative 1: CSS Selector Injection Manual
window.ealyxConfiguration = {
...
bannerCssSelector: "#ealyx-banner-widget",
productWidgetCssSelector: "#product-info-main .price-box",
cartSummaryCssSelector: "#cart-summary .ealyx-discount-box",
thankYouCssSelector: "#thank-you-page .ealyx-thankyou-widget",
...
};
Alternative 2: Targeted Placement
Use data-target to direct teasers into specific containers:
<!-- Teaser placeholder with target -->
<div class="ealyx-teaser"
data-type="product-widget"
data-target="#product-buybox-area">
</div>
<!-- Target container elsewhere on page -->
<div id="product-buybox-area">
<!-- Teaser will be injected here -->
</div>
Listener & Event Handling
Using Listeners for Custom Order Creation Timing
By default, orders are created when the webhook arrives. For more control, use the ealyx-ready-to-place-order event.
Tip: For a complete reference of all available SDK events and their usage patterns, see Available SDK Events in the Reference section.
Example:
// Listen for when trade-in is accepted and ready to place order
window.Ealyx?.addReadyToPlaceOrderListener((tradeInData) => {
console.log("Trade-in accepted, ready to place order", tradeInData);
// Custom logic: Create order immediately, before payment
fetch('/api/orders', {
method: 'POST',
body: JSON.stringify({
customer_id: currentCustomer.id,
trade_in_session: tradeInData.session_hash,
trade_in_value: tradeInData.valuation,
status: 'pending_payment' // Order exists but not confirmed yet
})
});
// Webhook will update status to 'confirmed' after payment
});
Why use this?
- Create orders immediately when trade-in is accepted (not waiting for webhook)
- Webhook updates status, doesn't create duplicate
- Better handling of race conditions
- More control over order creation flow
Payment Method Handling
Supporting Both Ealyx Pay and Other Methods Simultaneously
You can support both payment methods in the same checkout:
const paymentRadios = document.querySelectorAll('input[name="payment_method"]');
paymentRadios.forEach(radio => {
radio.addEventListener("change", () => {
const isEalyx = radio.value === "ealyx_pay";
const hasTrade = window.Ealyx?.hasActiveTradeIn?.() || false;
// Show/hide Ealyx option based on trade-in and user preference
if (isEalyx && !hasTrade) {
// No active trade-in, disable Ealyx Pay
radio.disabled = true;
alert("Please complete a trade-in valuation first");
} else {
radio.disabled = false;
window.Ealyx?.updatePaymentSelected(isEalyx);
}
});
});
// Handle form submission for both methods
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();
// Ealyx Pay modal opens, webhook handles order creation
return false;
}
// Other methods: form submits normally, you handle payment
});
Webhook Handling
Webhook Retry and Idempotency Patterns
For maximum reliability, implement idempotency in your webhook endpoint:
#!/bin/bash
# Check if we've already processed this webhook
check_idempotency() {
local webhook_id="$1"
local existing=$(mysql -e "SELECT id FROM ealyx_webhooks WHERE external_id='$webhook_id' LIMIT 1")
if [ ! -z "$existing" ]; then
echo "Already processed"
return 0 # Success (already done)
else
return 1 # Not processed yet
fi
}
# Handle webhook
process_webhook() {
local webhook_id=$(echo "$BODY" | jq -r '.id')
local cart_id=$(echo "$BODY" | jq -r '.cart_id')
local status=$(echo "$BODY" | jq -r '.status')
# Check if we've seen this before
if check_idempotency "$webhook_id"; then
return 200 # Return success without doing anything
fi
# Process webhook (create/update order)
mysql -e "INSERT INTO orders (cart_id, status) VALUES ('$cart_id', '$status')"
# Record that we've processed this
mysql -e "INSERT INTO ealyx_webhooks (external_id, received_at, processed) VALUES ('$webhook_id', NOW(), 1)"
return 200
}
Webhook Failure Handling & Recovery
Not all webhooks will succeed immediately. Here's how to handle failures gracefully:
Failure Scenarios:
-
Webhook endpoint is unreachable (timeout, DNS, network error)
- Ealyx will retry (Ealyx implements its own retry policy)
- Your job: Implement recovery mechanisms for orphaned orders
-
Webhook signature validation fails (wrong secret, parsing error)
- Webhook is rejected with HTTP 403
- Ealyx may retry, but the root cause must be fixed
- Check: Use correct secret format
{USER_ID}-{MERCHANT_ID}
-
Webhook endpoint returns non-200 status
- Ealyx will likely retry
- Your job: Log and investigate why your endpoint failed
-
Your endpoint processes webhook successfully but connection drops
- Webhook marked as failed from Ealyx perspective
- Your database already updated (idempotency prevents duplicates)
- Ealyx will retry, your idempotency check prevents duplicate updates
Recovery for Orphaned Orders:
An "orphaned order" is one where you received payment from Ealyx but the webhook never arrived or failed. Implement a reconciliation check:
#!/bin/bash
# Daily reconciliation: Find orders created via Ealyx Pay
# but not yet confirmed in Ealyx's system
reconcile_ealyx_orders() {
local token="$ACCESS_TOKEN" # Valid token with proper scope
# Get all orders from Ealyx that were paid via Ealyx Pay
# (orders API endpoint - check Ealyx documentation for exact endpoint)
ealyx_orders=$(curl -s -X GET '{API_BASE_URL}/payments/orders/' \
-H "Authorization: Bearer $token" \
-G \
--data-urlencode 'status=payment_completed' \
--data-urlencode 'limit=100')
# Get all payment_completed orders from your database
local_orders=$(mysql -e "SELECT order_id FROM orders WHERE payment_status='payment_completed' AND ealyx_confirmed=0")
# For each order in Ealyx that's not in local database
echo "$ealyx_orders" | jq -r '.orders[] | .order_id' | while read ealyx_order_id; do
if ! echo "$local_orders" | grep -q "$ealyx_order_id"; then
echo "⚠️ ORPHANED ORDER: $ealyx_order_id in Ealyx but not in local database"
# Option 1: Create the order in your database
order_data=$(echo "$ealyx_orders" | jq ".orders[] | select(.order_id == \"$ealyx_order_id\")")
order_amount=$(echo "$order_data" | jq '.amount')
customer_id=$(echo "$order_data" | jq '.customer_id')
mysql -e "INSERT INTO orders (order_id, customer_id, amount, payment_status, ealyx_confirmed) \
VALUES ('$ealyx_order_id', '$customer_id', '$order_amount', 'payment_completed', 1)"
echo "✓ Created missing order: $ealyx_order_id"
# Option 2: Alert admin if you prefer manual review
# mail -s "Orphaned order detected: $ealyx_order_id" admin@yoursite.com
fi
done
}
# Run daily via cron:
# 0 2 * * * /scripts/reconcile_ealyx.sh >> /var/log/ealyx_reconciliation.log
Monitoring Webhook Health:
#!/bin/bash
# Log all webhook events with status
log_webhook_event() {
local webhook_id="$1"
local status="$2"
local error_msg="$3"
mysql -e "INSERT INTO webhook_events (webhook_id, status, error_message, received_at) \
VALUES ('$webhook_id', '$status', '$error_msg', NOW())"
}
# Monitor webhook failures over time
check_webhook_health() {
# Get webhook failure rate in last 24 hours
local total=$(mysql -e "SELECT COUNT(*) FROM webhook_events WHERE received_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)")
local failed=$(mysql -e "SELECT COUNT(*) FROM webhook_events WHERE status='failed' AND received_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)")
if [ "$total" -gt 0 ]; then
local failure_rate=$((failed * 100 / total))
echo "Webhook failure rate (24h): $failure_rate% ($failed/$total)"
# Alert if failure rate exceeds threshold
if [ "$failure_rate" -gt 5 ]; then
echo "⚠️ HIGH FAILURE RATE: $(echo "$error_msg")"
# Send alert to ops team
mail -s "Ealyx webhook failure rate at $failure_rate%" alerts@yoursite.com
fi
fi
}
# Database schema for monitoring
cat << 'EOF'
CREATE TABLE webhook_events (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
webhook_id VARCHAR(255) NOT NULL,
status ENUM('received', 'processed', 'failed') NOT NULL,
error_message TEXT,
received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_webhook_id (webhook_id),
INDEX idx_received_at (received_at),
INDEX idx_status (status)
);
CREATE TABLE orphaned_orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
ealyx_order_id VARCHAR(255) UNIQUE NOT NULL,
detected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
resolved_at TIMESTAMP NULL,
resolution VARCHAR(255),
INDEX idx_ealyx_order_id (ealyx_order_id),
INDEX idx_resolved_at (resolved_at)
);
EOF
Webhook Failure Checklist:
Before assuming webhooks are broken, check:
- Webhook endpoint returns HTTP 200 (not 201, 204, or other 2xx)
- Signature validation secret is
{USER_ID}-{MERCHANT_ID}, NOT CLIENT_SECRET - Raw request body is used for signature (not parsed JSON)
- Endpoint doesn't require authentication (webhooks come from Ealyx servers)
- Endpoint timeout is longer than 30 seconds (Ealyx waits for confirmation)
- Database is not locked/deadlocked during webhook processing
- Logging is working to capture webhook events for debugging
Testing Webhook Locally:
# Simulate a webhook using curl from another machine or localhost
cat << 'EOF' > test_webhook.sh
#!/bin/bash
# Generate a test webhook payload
WEBHOOK_ID=$(uuidgen)
BODY='{
"id":"'$WEBHOOK_ID'",
"order_id":"ORD-TEST-'$(date +%s)'",
"status":"payment_completed",
"amount":8999,
"currency":"EUR"
}'
# Calculate signature
USER_ID="your_user_id"
MERCHANT_ID="your_merchant_id"
SECRET="$USER_ID-$MERCHANT_ID"
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" \
-v
# Check response
echo "HTTP Status: $?"
EOF
chmod +x test_webhook.sh
./test_webhook.sh
Valuation Reactivation
Reusing Expired Valuations for Customer Re-engagement
When customers leave without completing a purchase, you can use stored valuation data to re-engage them:
// Store valuation details when customer accepts
window.addEventListener("ealyx-valuation-updated", (event) => {
const valuation = event.detail;
// Save to database for later re-engagement
fetch('/api/customer/valuations', {
method: 'POST',
body: JSON.stringify({
customer_id: currentCustomer.id,
product: valuation.product,
condition: valuation.condition,
value: valuation.value,
expires_at: valuation.valid_until
})
});
});
// Later: Show email reminder with stored valuation
// "Your iPhone 15 is worth €350! Complete your purchase in 3 days before offer expires"
Multiple Token Refresh Strategies
Proactive Refresh (Best for Active Sessions)
# Refresh token before it expires (prevent 401 errors)
# Check token expiration every request
check_and_refresh_token() {
local expires_at=$(mysql -e "SELECT expires_at FROM ealyx_tokens LIMIT 1" | tail -1)
local now=$(date +%s)
local time_left=$((expires_at - now))
# Refresh when less than 5 minutes remain
if [ $time_left -lt 300 ]; then
refresh_token_immediately
fi
}
# Every API request does this first
curl -X GET "$API_BASE_URL/core/merchants/" \
-H "Authorization: Bearer $(check_and_refresh_token)"
Reactive Refresh (Best for Low-Traffic Sites)
# Only refresh if you get a 401 response
make_api_request() {
local endpoint="$1"
# Try with current token
local response=$(curl -s -w "\n%{http_code}" \
-X GET "$API_BASE_URL$endpoint" \
-H "Authorization: Bearer $ACCESS_TOKEN")
local body=$(echo "$response" | head -n -1)
local status=$(echo "$response" | tail -n 1)
if [ "$status" = "401" ]; then
# Token expired, refresh and retry
refresh_token
response=$(curl -s -w "\n%{http_code}" \
-X GET "$API_BASE_URL$endpoint" \
-H "Authorization: Bearer $ACCESS_TOKEN")
fi
echo "$response"
}