Webhooks API
The Webhooks API enables real-time order lifecycle notifications to be sent to merchant systems.
Overview
Webhooks allow your systems to receive notifications when events occur, such as:
- Order created
- Order updated
- Order fulfilled
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ Lorn AI │────▶│ Webhook │────▶│ Merchant │
│ Gateway │ │ with HMAC │ │ System │
│ │ │ Signature │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘Emit Webhook
Emit a webhook event to a target URL.
POST /webhooks/emitRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
event_type | string | Yes | Event type (e.g., order.created) |
target_url | string | No | URL to POST the webhook to |
checkout_session_id | string | No | Session ID to include in payload |
payload | object | No | Custom payload data |
signature_secret | string | No | Secret for signing (default: demo_webhook_secret) |
Response
{
"delivered": 200,
"signature": "t=1705312260,v1=abc123...",
"payload": {
"id": "evt_abc123",
"type": "order.created",
"created": "2024-01-15T10:30:00Z",
"data": {
"object": { ... }
}
}
}| Field | Type | Description |
|---|---|---|
delivered | integer/null | HTTP status code from target, or null if not sent |
signature | string | HMAC signature for verification |
payload | object | The webhook payload that was/would be sent |
Examples
cURL
# Emit order.created webhook
curl -X POST "https://{{YOUR_STORE_URL}}/webhooks/emit" \
-H "Content-Type: application/json" \
-H "X-ACP-API-Key: {{YOUR_API_KEY}}" \
-d '{
"event_type": "order.created",
"checkout_session_id": "{{SESSION_ID}}",
"target_url": "https://your-merchant.com/webhooks/lorn",
"signature_secret": "your_webhook_secret"
}'Python
import requests
BASE_URL = "https://{{YOUR_STORE_URL}}"
HEADERS = {
"Content-Type": "application/json",
"X-ACP-API-Key": "{{YOUR_API_KEY}}"
}
def emit_webhook(
event_type: str,
checkout_session_id: str = None,
target_url: str = None,
payload: dict = None,
signature_secret: str = None
) -> dict:
body = {"event_type": event_type}
if checkout_session_id:
body["checkout_session_id"] = checkout_session_id
if target_url:
body["target_url"] = target_url
if payload:
body["payload"] = payload
if signature_secret:
body["signature_secret"] = signature_secret
response = requests.post(
f"{BASE_URL}/webhooks/emit",
json=body,
headers=HEADERS
)
response.raise_for_status()
return response.json()
# Emit order.created with session data
result = emit_webhook(
event_type="order.created",
checkout_session_id="cs_demo_abc123",
target_url="https://your-merchant.com/webhooks/lorn"
)
print(f"Delivered: {result['delivered']}")
print(f"Signature: {result['signature']}")TypeScript
const BASE_URL = "https://{{YOUR_STORE_URL}}";
const API_KEY = "{{YOUR_API_KEY}}";
interface EmitWebhookInput {
event_type: string;
checkout_session_id?: string;
target_url?: string;
payload?: object;
signature_secret?: string;
}
interface EmitWebhookResponse {
delivered: number | null;
signature: string;
payload: object;
}
async function emitWebhook(input: EmitWebhookInput): Promise<EmitWebhookResponse> {
const response = await fetch(`${BASE_URL}/webhooks/emit`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-ACP-API-Key": API_KEY
},
body: JSON.stringify(input)
});
if (!response.ok) {
throw new Error(`Failed to emit webhook: ${response.statusText}`);
}
return response.json();
}
// Usage
const result = await emitWebhook({
event_type: "order.created",
checkout_session_id: "cs_demo_abc123",
target_url: "https://your-merchant.com/webhooks/lorn"
});
console.log(`Delivered with status: ${result.delivered}`);Java
public String emitWebhook(String eventType, String sessionId, String targetUrl) throws Exception {
String requestBody = String.format("""
{
"event_type": "%s",
"checkout_session_id": "%s",
"target_url": "%s"
}
""", eventType, sessionId, targetUrl);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/webhooks/emit"))
.header("Content-Type", "application/json")
.header("X-ACP-API-Key", API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
return response.body();
}Go
type EmitWebhookInput struct {
EventType string `json:"event_type"`
CheckoutSessionID string `json:"checkout_session_id,omitempty"`
TargetURL string `json:"target_url,omitempty"`
Payload map[string]interface{} `json:"payload,omitempty"`
SignatureSecret string `json:"signature_secret,omitempty"`
}
func emitWebhook(input EmitWebhookInput) (map[string]interface{}, error) {
body, err := json.Marshal(input)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", baseURL+"/webhooks/emit", bytes.NewBuffer(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ACP-API-Key", apiKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result map[string]interface{}
json.Unmarshal(respBody, &result)
return result, nil
}Event Types
| Event Type | Description |
|---|---|
order.created | Order was placed |
order.updated | Order status changed |
order.fulfilled | Order was shipped/fulfilled |
order.canceled | Order was canceled |
order.refunded | Order was refunded |
Webhook Payload Structure
{
"id": "evt_abc123def456",
"type": "order.created",
"created": "2024-01-15T10:35:00Z",
"data": {
"object": {
"id": "cs_demo_abc123",
"status": "completed",
"currency": "USD",
"line_items": [...],
"amounts": {
"subtotal": 59.98,
"tax": 4.80,
"shipping": 7.99,
"total": 72.77
},
...
}
}
}Webhook Signatures
Webhooks are signed using HMAC-SHA256 to verify authenticity.
Signature Header
Webhooks include the X-Webhook-Signature header:
X-Webhook-Signature: t=1705312260,v1=abc123def456...Format: t=<timestamp>,v1=<hex_digest>
Verifying Signatures
Python
import hmac
import hashlib
def verify_webhook_signature(
body: bytes,
signature_header: str,
secret: str,
tolerance_seconds: int = 300
) -> bool:
"""Verify a webhook signature."""
# Parse signature header
parts = dict(p.split('=', 1) for p in signature_header.split(','))
timestamp = int(parts.get('t', 0))
received_sig = parts.get('v1', '')
# Check timestamp freshness (prevent replay attacks)
import time
if abs(time.time() - timestamp) > tolerance_seconds:
return False
# Compute expected signature
expected_sig = hmac.new(
secret.encode('utf-8'),
body,
hashlib.sha256
).hexdigest()
# Timing-safe comparison
return hmac.compare_digest(expected_sig, received_sig)
# Usage in a Flask webhook handler
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"
@app.route('/webhooks/lorn', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature', '')
if not verify_webhook_signature(request.data, signature, WEBHOOK_SECRET):
return {'error': 'Invalid signature'}, 401
event = request.json
event_type = event.get('type')
if event_type == 'order.created':
# Handle new order
order = event['data']['object']
print(f"New order: {order['id']}")
return {'received': True}TypeScript
import crypto from 'crypto';
function verifyWebhookSignature(
body: string,
signatureHeader: string,
secret: string,
toleranceSeconds: number = 300
): boolean {
// Parse signature header
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
);
const timestamp = parseInt(parts.t || '0');
const receivedSig = parts.v1 || '';
// Check timestamp freshness
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) {
return false;
}
// Compute expected signature
const expectedSig = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
// Timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(expectedSig),
Buffer.from(receivedSig)
);
}
// Usage in Express
import express from 'express';
const app = express();
const WEBHOOK_SECRET = 'your_webhook_secret';
app.post('/webhooks/lorn', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'] as string;
if (!verifyWebhookSignature(req.body.toString(), signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body.toString());
switch (event.type) {
case 'order.created':
console.log(`New order: ${event.data.object.id}`);
break;
case 'order.fulfilled':
console.log(`Order fulfilled: ${event.data.object.id}`);
break;
}
res.json({ received: true });
});Best Practices
1. Verify All Signatures
Always verify the webhook signature before processing:
if not verify_webhook_signature(body, signature, secret):
return 4012. Respond Quickly
Return a 200 response as soon as possible, then process asynchronously:
@app.route('/webhooks/lorn', methods=['POST'])
def handle_webhook():
# Verify signature
if not verify_signature(...):
return 401
# Queue for async processing
queue.enqueue(process_webhook, request.json)
# Return immediately
return {'received': True}3. Handle Retries
Webhooks may be retried. Use idempotency to prevent duplicate processing:
def process_webhook(event):
event_id = event['id']
# Check if already processed
if redis.get(f"webhook:{event_id}"):
return # Already handled
# Process the event
handle_event(event)
# Mark as processed (with TTL)
redis.setex(f"webhook:{event_id}", 86400, "1")4. Log Events
Keep an audit trail:
import logging
logger = logging.getLogger('webhooks')
def process_webhook(event):
logger.info(f"Received webhook: type={event['type']} id={event['id']}")
try:
handle_event(event)
logger.info(f"Processed webhook: id={event['id']}")
except Exception as e:
logger.error(f"Failed to process webhook: id={event['id']} error={e}")
raiseErrors
| Status | Error | Description |
|---|---|---|
| 400 | invalid_request | Missing required fields |
| 502 | bad_gateway | Failed to deliver to target URL |
Error Response
{
"detail": "Failed to deliver webhook: Connection refused"
}Testing Webhooks
Without Target URL
Omit target_url to get the signed payload without delivery:
curl -X POST "https://{{YOUR_STORE_URL}}/webhooks/emit" \
-H "Content-Type: application/json" \
-H "X-ACP-API-Key: {{YOUR_API_KEY}}" \
-d '{
"event_type": "order.created",
"checkout_session_id": "{{SESSION_ID}}"
}'Response:
{
"delivered": null,
"signature": "t=1705312260,v1=...",
"payload": { ... }
}Use this to verify your signature verification logic.