Webhook Integration
Receive real-time notifications about delivery events via webhooks.
Overview
KioskForce delivery lockers can send HTTP POST requests to your server when important events occur, such as:
- Package drop-off completed
- Package picked up by customer
- Reservation cancelled
- Pickup abandoned
- Pickup attempt failed
- Out-of-band door opening completed (customer support)
This allows you to update your order status in real-time without polling the API.
Webhook Configuration
Global Webhook URL
Set up a global webhook URL during platform onboarding that will receive all events by default.
Per-Reservation Webhooks
Override the global webhook URL for specific reservations by including webhook_url in your reservation request. The URL must use HTTPS:
{
"machine_id": "kf_tangerang_001",
"platform_order_id": "order_12345",
"webhook_url": "https://yourapi.com/webhooks/delivery",
"packaging": { ... }
}
Webhook Payload Structure
All webhook events are wrapped in a common Payload structure that provides metadata for event tracking and idempotency.
Payload Wrapper
Every webhook request body follows this structure:
{
"event_id": "evt_550e8400-e29b-41d4-a716-446655440000",
"idempotency_key": "idem_abc123def456",
"event_time_unix_timestamp": 1718271000,
"event_time_timestamp": "2025-06-13T10:30:00Z",
"platform_order_id": "order_12345",
"reservation_id": "res_abc123",
"event_type": "EVENT_TYPE_DROPOFF",
"drop_off_event": { ... }
}
| Field | Type | Required | Description |
|---|---|---|---|
event_id | string | Yes | Unique identifier for this event delivery |
idempotency_key | string | Yes | Unique key across multiple retries - use this for deduplication |
event_time_unix_timestamp | int64 | Yes | Unix timestamp (seconds) when the event occurred |
event_time_timestamp | string | Yes | ISO 8601 timestamp when the event occurred |
platform_order_id | string | Yes | Your original order ID from the reservation |
reservation_id | string | Yes | KioskForce reservation ID |
event_type | enum | Yes | Type of event (see Event Types below) |
*_event | object | Yes | Event-specific data (one of the event types below) |
Event Types
| Event Type | Field Name | Description |
|---|---|---|
EVENT_TYPE_RESERVATION_CREATED | reservation_created_event | Reservation created |
EVENT_TYPE_DROPOFF | drop_off_event | Package dropped off |
EVENT_TYPE_PICKUP | pick_up_event | Package picked up |
EVENT_TYPE_RESERVATION_CANCELLED | reservation_cancelled_event | Reservation cancelled |
EVENT_TYPE_PICKUP_FAILED | pick_up_failed_event | Pickup attempt failed |
EVENT_TYPE_OPEN_DOOR_FULFILLED | open_door_fulfilled_event | Door open request completed |
Common Data Structures
Cell Object
Represents a locker cell where packages are stored.
{
"description": "A03",
"cell_id": "cell_kf_tan_001_a03"
}
| Field | Type | Required | Description |
|---|---|---|---|
description | string | Yes | Human-readable cell label (e.g., "A03", "B05") |
cell_id | string | Yes | Unique identifier for the cell within a machine |
Machine Object
Represents a delivery locker machine.
{
"machine_id": "kf_tangerang_001",
"name": "Tangerang Station Locker",
"location": "Tangerang Train Station, Main Hall",
"latitude": -6.1781,
"longitude": 106.6319,
"online": true,
"preferred_cells": [
{
"width": 400,
"height": 300,
"depth": 500,
"heating": false,
"cooling": false,
"weight_sensor": true
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
machine_id | string | Yes | Unique machine identifier |
name | string | Yes | Human-readable machine name |
location | string | Yes | Machine location description |
latitude | float | Yes | GPS latitude coordinate |
longitude | float | Yes | GPS longitude coordinate |
online | boolean | Yes | Whether the machine is currently online |
preferred_cells | array | No | Available cell configurations (see CellInformation) |
CellInformation Object
Describes cell dimensions and features.
{
"width": 400,
"height": 300,
"depth": 500,
"heating": false,
"cooling": true,
"weight_sensor": true
}
| Field | Type | Required | Description |
|---|---|---|---|
width | int32 | Yes | Cell width in millimeters |
height | int32 | Yes | Cell height in millimeters |
depth | int32 | Yes | Cell depth in millimeters |
heating | boolean | Yes | Whether the cell has heating capability |
cooling | boolean | Yes | Whether the cell has cooling capability |
weight_sensor | boolean | Yes | Whether the cell has a weight sensor |
SourceType Enum
Indicates how the user interacted with the machine.
| Value | Description |
|---|---|
SOURCE_TYPE_DELIVERY_PLATFORM | Via delivery platform API call |
SOURCE_TYPE_MACHINE | Direct interaction at machine (QR code scan or numeric code entry) |
Webhook Events
1. Reservation Created Event
Triggered when a new reservation is successfully created. This event contains the codes and QR codes needed for drop-off and pickup.
{
"event_id": "evt_550e8400-e29b-41d4-a716-446655440000",
"idempotency_key": "idem_res_created_abc123",
"event_time_unix_timestamp": 1718258400,
"event_time_timestamp": "2025-06-13T09:00:00Z",
"platform_order_id": "order_12345",
"reservation_id": "res_abc123",
"event_type": "EVENT_TYPE_RESERVATION_CREATED",
"reservation_created_event": {
"reservation_id": "res_abc123",
"machine_id": "kf_tangerang_001",
"machine": {
"machine_id": "kf_tangerang_001",
"name": "Tangerang Station Locker",
"location": "Tangerang Train Station, Main Hall",
"latitude": -6.1781,
"longitude": 106.6319,
"online": true
},
"drop_off_code": "123456",
"drop_off_qrcode": "KFDO-ABC123XYZ",
"drop_off_qrcode_url": "https://api.delivery.kioskforce.com/qrcode/KFDO-ABC123XYZ",
"pickup_code": "789012",
"pickup_code_qrcode": "KFPU-DEF456UVW",
"pickup_code_qrcode_url": "https://api.delivery.kioskforce.com/qrcode/KFPU-DEF456UVW"
}
}
Event-Specific Fields:
| Field | Type | Required | Description |
|---|---|---|---|
reservation_id | string | Yes | Reservation identifier |
machine_id | string | Yes | Machine identifier |
machine | Machine | Yes | Full machine details |
drop_off_code | string | Yes | Numeric code for driver to unlock door |
drop_off_qrcode | string | Yes | QR code content for driver (render in app) |
drop_off_qrcode_url | string | Yes | URL to QR code image for driver |
pickup_code | string | Yes | Numeric code for customer to unlock door |
pickup_code_qrcode | string | Yes | QR code content for customer (render in app) |
pickup_code_qrcode_url | string | Yes | URL to QR code image for customer |
Use Cases:
- Send pickup codes to customers via SMS/email/push notification
- Display QR codes in your mobile app for driver and customer
- Store codes for customer support reference
2. Drop-off Event
Triggered when a driver/rider successfully drops off a package and closes the door.
⚠️ Important Timing Note:
- This event is sent when the locker door closes after the dropoff
- The door opening alone does NOT trigger this event
- This ensures the package has been successfully placed before notifying the platform
{
"event_id": "evt_660e8400-e29b-41d4-a716-446655440001",
"idempotency_key": "idem_dropoff_abc123",
"event_time_unix_timestamp": 1718271000,
"event_time_timestamp": "2025-06-13T10:30:00Z",
"platform_order_id": "order_12345",
"reservation_id": "res_abc123",
"event_type": "EVENT_TYPE_DROPOFF",
"drop_off_event": {
"source_type": "SOURCE_TYPE_MACHINE",
"cell": {
"description": "A03",
"cell_id": "cell_kf_tan_001_a03"
},
"machine": {
"machine_id": "kf_tangerang_001",
"name": "Tangerang Station Locker",
"location": "Tangerang Train Station, Main Hall",
"latitude": -6.1781,
"longitude": 106.6319,
"online": true
}
}
}
Note: Fields with empty or zero values are omitted from the actual payload. For example,
request_idonly appears when the action was initiated via API (SOURCE_TYPE_DELIVERY_PLATFORM).
Event-Specific Fields:
| Field | Type | Required | Description |
|---|---|---|---|
source_type | SourceType | Yes | How the drop-off was initiated |
request_id | string | No | X-Request-ID (only present if initiated via API) |
cell | Cell | Yes | Cell where package was placed |
machine | Machine | Yes | Machine details |
3. Pickup Event
Triggered when a customer successfully picks up their package.
⚠️ Important Timing Note:
- This event is sent immediately when the locker door opens successfully
- This is different from dropoff - the event is sent on door open, not door close
- The reservation state also changes to
PICKED_UPimmediately when the door opens
{
"event_id": "evt_770e8400-e29b-41d4-a716-446655440002",
"idempotency_key": "idem_pickup_abc123",
"event_time_unix_timestamp": 1718279100,
"event_time_timestamp": "2025-06-13T12:45:00Z",
"platform_order_id": "order_12345",
"reservation_id": "res_abc123",
"event_type": "EVENT_TYPE_PICKUP",
"pick_up_event": {
"source_type": "SOURCE_TYPE_MACHINE",
"cell": {
"description": "A03",
"cell_id": "cell_kf_tan_001_a03"
},
"machine": {
"machine_id": "kf_tangerang_001",
"name": "Tangerang Station Locker",
"location": "Tangerang Train Station, Main Hall",
"latitude": -6.1781,
"longitude": 106.6319,
"online": true
}
}
}
Note: Fields with empty or zero values are omitted.
request_idonly appears when initiated via API.
Event-Specific Fields:
| Field | Type | Required | Description |
|---|---|---|---|
source_type | SourceType | Yes | How the pickup was initiated |
request_id | string | No | X-Request-ID (only present if initiated via API) |
cell | Cell | Yes | Cell where package was picked up from |
machine | Machine | Yes | Machine details |
4. Reservation Cancelled Event
Triggered when a reservation is cancelled before drop-off.
{
"event_id": "evt_880e8400-e29b-41d4-a716-446655440003",
"idempotency_key": "idem_cancelled_abc123",
"event_time_unix_timestamp": 1718254500,
"event_time_timestamp": "2025-06-13T08:15:00Z",
"platform_order_id": "order_12345",
"reservation_id": "res_abc123",
"event_type": "EVENT_TYPE_RESERVATION_CANCELLED",
"reservation_cancelled_event": {
"reservation_id": "res_abc123",
"machine_id": "kf_tangerang_001",
"machine": {
"machine_id": "kf_tangerang_001",
"name": "Tangerang Station Locker",
"location": "Tangerang Train Station, Main Hall",
"latitude": -6.1781,
"longitude": 106.6319,
"online": true
},
"cancelled_at": "2025-06-13T08:15:00Z",
"cancellation_party": "CANCELLATION_PARTY_DELIVERY_PLATFORM"
}
}
Event-Specific Fields:
| Field | Type | Required | Description |
|---|---|---|---|
reservation_id | string | Yes | Reservation identifier |
machine_id | string | Yes | Machine identifier |
machine | Machine | Yes | Machine details |
cancelled_at | timestamp | Yes | When the cancellation occurred |
cancellation_party | enum | Yes | Who cancelled the reservation |
Cancellation Party Values:
| Value | Description |
|---|---|
CANCELLATION_PARTY_DELIVERY_PLATFORM | Cancelled by your platform via API |
CANCELLATION_PARTY_KF_PLATFORM | Cancelled by KioskForce (e.g., expiry, maintenance) |
5. Pickup Failed Event
Triggered when a pickup attempt fails (door doesn't open).
{
"event_id": "evt_990e8400-e29b-41d4-a716-446655440004",
"idempotency_key": "idem_pickup_failed_abc123",
"event_time_unix_timestamp": 1731335400,
"event_time_timestamp": "2025-11-11T14:30:00Z",
"platform_order_id": "order_12345",
"reservation_id": "res_abc123",
"event_type": "EVENT_TYPE_PICKUP_FAILED",
"pick_up_failed_event": {
"source_type": "SOURCE_TYPE_MACHINE",
"cell": {
"description": "A03",
"cell_id": "cell_kf_tan_001_a03"
},
"machine": {
"machine_id": "kf_tangerang_001",
"name": "Tangerang Station Locker",
"location": "Tangerang Train Station, Main Hall",
"latitude": -6.1781,
"longitude": 106.6319,
"online": true
},
"error_message": "Door mechanism failure - maintenance required"
}
}
Note: Fields with empty or zero values are omitted.
request_idonly appears when initiated via API.
Event-Specific Fields:
| Field | Type | Required | Description |
|---|---|---|---|
source_type | SourceType | Yes | How the pickup was attempted |
request_id | string | No | X-Request-ID (only present if initiated via API) |
cell | Cell | Yes | Cell where pickup failed |
machine | Machine | Yes | Machine details |
error_message | string | No | Details about the failure (omitted if empty) |
Use Cases:
- Alert platform about failed pickup attempts for customer support
- Track machine/door reliability and maintenance needs
- Proactive customer communication about pickup issues
- Detect potential fraud or tampering attempts
- Trigger automated incident reports
6. OpenDoor Fulfilled Event
Triggered when an out-of-band door opening request completes (via the OpenDoor API).
{
"event_id": "evt_aa0e8400-e29b-41d4-a716-446655440005",
"idempotency_key": "idem_opendoor_abc123",
"event_time_unix_timestamp": 1731509100,
"event_time_timestamp": "2025-11-13T15:45:00Z",
"platform_order_id": "order_12345",
"reservation_id": "res_abc123",
"event_type": "EVENT_TYPE_OPEN_DOOR_FULFILLED",
"open_door_fulfilled_event": {
"reservation_id": "res_abc123",
"request_id": "req_xyz789",
"cell": {
"description": "A03",
"cell_id": "cell_kf_tan_001_a03"
},
"success": true,
"fulfilled_at": "2025-11-13T15:45:00Z",
"reason": "Customer support request - customer unable to open door",
"platform_reference_id": "support_ticket_456",
"machine": {
"machine_id": "kf_tangerang_001",
"name": "Tangerang Station Locker",
"location": "Tangerang Train Station, Main Hall",
"latitude": -6.1781,
"longitude": 106.6319,
"online": true
}
}
}
Note: Fields with empty or zero values are omitted.
error_messageonly appears whensuccess = false.
Event-Specific Fields:
| Field | Type | Required | Description |
|---|---|---|---|
reservation_id | string | Yes | Reservation identifier |
request_id | string | Yes | X-Request-ID from original OpenDoor API call |
cell | Cell | Yes | Cell that was opened (or attempted) |
success | boolean | Yes | Whether door opened successfully |
fulfilled_at | timestamp | Yes | When the operation completed |
error_message | string | No | Error details (only present if success = false) |
reason | string | No | Reason echoed from original request (omitted if empty) |
platform_reference_id | string | No | Your reference ID from original request (omitted if empty) |
machine | Machine | Yes | Machine details |
Use Cases:
- Confirm door opening for customer support tickets
- Track success rate of manual interventions
- Alert support staff on repeated failures requiring physical intervention
- Audit trail for all support-initiated door operations
- Correlate support actions with customer complaints
Error Handling Example:
{
"event_id": "evt_bb0e8400-e29b-41d4-a716-446655440006",
"idempotency_key": "idem_opendoor_failed_abc123",
"event_time_unix_timestamp": 1731509100,
"event_time_timestamp": "2025-11-13T15:45:00Z",
"platform_order_id": "order_12345",
"reservation_id": "res_abc123",
"event_type": "EVENT_TYPE_OPEN_DOOR_FULFILLED",
"open_door_fulfilled_event": {
"reservation_id": "res_abc123",
"request_id": "req_xyz789",
"cell": {
"description": "A03",
"cell_id": "cell_kf_tan_001_a03"
},
"success": false,
"fulfilled_at": "2025-11-13T15:45:00Z",
"error_message": "Device offline - machine not responding",
"machine": {
"machine_id": "kf_tangerang_001",
"name": "Tangerang Station Locker",
"location": "Tangerang Train Station, Main Hall",
"latitude": -6.1781,
"longitude": 106.6319,
"online": false
}
}
}
---
## Webhook Timing: Dropoff vs Pickup
The timing of webhook events differs between dropoff and pickup operations:
| Event Type | Triggered When | State Transition | Reason |
|------------|----------------|------------------|--------|
| **Dropoff** | Door **closes** | `PENDING` → `DROPPED_OFF` | Ensures package was placed successfully |
| **Pickup** | Door **opens** | `DROPPED_OFF` → `PICKED_UP` | Consumer has accessed the package |
### Example Timeline: Dropoff
10:00:00 - API call /dropoffs → Door opens (state: PENDING, no webhook) 10:00:15 - Driver places package 10:00:30 - Door closes → State: DROPPED_OFF + Dropoff webhook sent
### Example Timeline: Pickup
12:00:00 - API call /pickups → Door opens → State: PICKED_UP + Pickup webhook sent immediately 12:00:15 - Consumer retrieves package 12:00:25 - Door closes
### Integration Considerations
1. **Don't assume immediate dropoff**: After calling `/dropoffs`, wait for the webhook before updating your order status to "delivered"
2. **Pickup is immediate**: The pickup webhook arrives as soon as the door opens successfully
3. **Grace periods**: Both operations allow door re-opening, but pickup state has already changed
## Webhook Authentication
The webhook system uses **HMAC-SHA256 signatures** to authenticate webhook payloads. This allows your platform to verify that webhook requests genuinely originate from KioskForce and haven't been tampered with.
### Signature Format
The signature is included in the `X-Webhook-Signature` header with the format:
```text
sha256=<hex-encoded-hash>
The signature is computed using:
- Algorithm: HMAC-SHA256
- Input: The raw JSON payload bytes
- Secret: Your webhook API key (provided during platform onboarding)
Verifying Signatures
Always verify the signature before processing webhook events. Here are examples in common languages:
Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strings"
)
func verifyWebhookSignature(payload []byte, signature, secret string) bool {
// Remove "sha256=" prefix
expectedSig := strings.TrimPrefix(signature, "sha256=")
// Compute HMAC-SHA256
h := hmac.New(sha256.New, []byte(secret))
h.Write(payload)
computedSig := hex.EncodeToString(h.Sum(nil))
// Constant-time comparison to prevent timing attacks
return hmac.Equal([]byte(expectedSig), []byte(computedSig))
}
Python
import hmac
import hashlib
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
expected_sig = signature.removeprefix("sha256=")
computed_sig = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_sig, computed_sig)
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSig = signature.replace('sha256=', '');
const computedSig = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expectedSig),
Buffer.from(computedSig)
);
}
Webhook Headers
Each webhook request includes these headers:
| Header | Description | Example |
|---|---|---|
Content-Type | Always JSON | application/json |
User-Agent | Service identifier | delivery-webhook/1.0 |
X-Platform-Order-Id | Your original order ID | order_12345 |
X-Webhook-Event | Event type (proto enum) | EVENT_TYPE_DROPOFF |
X-Webhook-ID | Unique event identifier (UUID) | 550e8400-e29b-41d4-a716-446655440000 |
X-Webhook-Timestamp | Unix timestamp (seconds) | 1737558150 |
X-Webhook-Signature | HMAC-SHA256 signature | sha256=a1b2c3d4... |
X-Webhook-Event Values:
| Event Type Header | Description |
|---|---|
EVENT_TYPE_RESERVATION_CREATED | Reservation created |
EVENT_TYPE_DROPOFF | Package dropped off |
EVENT_TYPE_PICKUP | Package picked up |
EVENT_TYPE_RESERVATION_CANCELLED | Reservation cancelled |
EVENT_TYPE_PICKUP_FAILED | Pickup attempt failed |
EVENT_TYPE_OPEN_DOOR_FULFILLED | Door open request completed |
Example Request
POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
User-Agent: delivery-webhook/1.0
X-Platform-Order-Id: order_12345
X-Webhook-Event: EVENT_TYPE_DROPOFF
X-Webhook-ID: 550e8400-e29b-41d4-a716-446655440000
X-Webhook-Timestamp: 1737558150
X-Webhook-Signature: sha256=a1b2c3d4e5f6...
Webhook Endpoint Requirements
Your webhook endpoint must:
0. Use HTTPS
Webhook URLs must use HTTPS for security. HTTP URLs will be rejected.
✅ Correct: https://yourapi.com/webhooks/delivery
❌ Invalid: http://yourapi.com/webhooks/delivery
1. Respond with HTTP 200
Always return HTTP status 200 for successful processing.
// Express.js example
app.post('/webhooks/delivery', (req, res) => {
try {
const event = req.body;
console.log('Received webhook:', event);
// Process the event
processDeliveryEvent(event);
// Return 200 status
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Processing failed' });
}
});
2. Handle Duplicate Events
Implement idempotency using the idempotency_key field from the payload (recommended). This key remains the same across retries for the same event:
const processedEvents = new Set();
function processDeliveryEvent(req) {
const payload = req.body;
// Use idempotency_key from payload (same across retries)
const idempotencyKey = payload.idempotency_key;
// Alternatively, use event_id for unique delivery tracking
// const eventId = payload.event_id;
if (processedEvents.has(idempotencyKey)) {
console.log('Duplicate event ignored:', idempotencyKey);
return;
}
processedEvents.add(idempotencyKey);
// Process the event based on event_type
switch (payload.event_type) {
case 'EVENT_TYPE_DROPOFF':
handleDropoff(payload.drop_off_event);
break;
case 'EVENT_TYPE_PICKUP':
handlePickup(payload.pick_up_event);
break;
case 'EVENT_TYPE_RESERVATION_CREATED':
handleReservationCreated(payload.reservation_created_event);
break;
case 'EVENT_TYPE_RESERVATION_CANCELLED':
handleReservationCancelled(payload.reservation_cancelled_event);
break;
case 'EVENT_TYPE_PICKUP_FAILED':
handlePickupFailed(payload.pick_up_failed_event);
break;
case 'EVENT_TYPE_OPEN_DOOR_FULFILLED':
handleOpenDoorFulfilled(payload.open_door_fulfilled_event);
break;
}
}
Key Fields for Idempotency:
| Field | Description | Use Case |
|---|---|---|
idempotency_key | Same across retries | Deduplicate webhook processing |
event_id | Unique per delivery attempt | Track individual delivery attempts |
3. Verify Signature (Required)
Verify the X-Webhook-Signature header to ensure webhook authenticity and payload integrity:
const crypto = require('crypto');
app.post('/webhooks/delivery', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const payload = JSON.stringify(req.body);
// Verify signature
if (!verifyWebhookSignature(payload, signature, YOUR_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Reject old webhooks (optional but recommended)
const webhookAge = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (webhookAge > 300) { // 5 minutes
return res.status(401).json({ error: 'Webhook timestamp too old' });
}
// Process webhook...
});
function verifyWebhookSignature(payload, signature, secret) {
const expectedSig = signature.replace('sha256=', '');
const computedSig = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expectedSig),
Buffer.from(computedSig)
);
}
Retry Logic
KioskForce will retry failed webhook deliveries with exponential backoff:
- Max retries: 5 attempts
- Backoff formula:
min(30 × 2^retry_count, 3600)seconds - Dead letter queue: After max retries, events move to a failed queue for manual review
| Attempt | Delay | Cumulative Time |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 30s | 30s |
| 3 | 60s | ~1.5 min |
| 4 | 120s | ~3.5 min |
| 5 | 240s | ~7.5 min |
A webhook is considered failed if:
- HTTP status is not 200-299
- Request times out (30 seconds)
- Connection cannot be established
Testing Webhooks
1. Use a Webhook Testing Service
For development, use services like:
- ngrok - Tunnel localhost to public URL
- webhook.site - Temporary webhook URLs
- RequestBin - Inspect webhook payloads
# Install and run ngrok
npm install -g ngrok
ngrok http 3000
# Use the generated URL in your webhook_url
# Example: https://abc123.ngrok.io/webhooks/delivery
2. Test with Real Reservations
Create a test reservation with your webhook URL:
curl -X POST "https://api.delivery.kioskforce.com/api/delivery/external/v1/reservations" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"machine_id": "test_machine",
"platform_order_id": "test_order_123",
"webhook_url": "https://abc123.ngrok.io/webhooks/delivery",
"packaging": {
"width": 300,
"height": 200,
"depth": 400
}
}'
Webhook Event Processing Examples
Order Status Updates
function handleDropoff(payload) {
const dropOffEvent = payload.drop_off_event;
// Update order status to "delivered"
updateOrderStatus(payload.platform_order_id, 'delivered');
// Send customer notification with cell info
sendCustomerNotification(payload.platform_order_id, {
message: `Your package has been delivered to locker ${dropOffEvent.cell.description}`,
machine_name: dropOffEvent.machine.name,
location: dropOffEvent.machine.location,
pickup_instructions: 'Use your pickup code to retrieve the package'
});
}
function handlePickup(payload) {
const pickUpEvent = payload.pick_up_event;
// Update order status to "completed"
updateOrderStatus(payload.platform_order_id, 'completed');
// Send completion notification
sendCustomerNotification(payload.platform_order_id, {
message: 'Thank you! Your package has been successfully picked up.'
});
}
function handleReservationCreated(payload) {
const reservationEvent = payload.reservation_created_event;
// Store pickup codes for customer
storePickupCodes(payload.platform_order_id, {
pickup_code: reservationEvent.pickup_code,
pickup_qrcode_url: reservationEvent.pickup_code_qrcode_url,
machine_name: reservationEvent.machine.name,
machine_location: reservationEvent.machine.location
});
// Send codes to customer
sendCustomerNotification(payload.platform_order_id, {
message: 'Your delivery locker is reserved!',
pickup_code: reservationEvent.pickup_code,
qrcode_url: reservationEvent.pickup_code_qrcode_url
});
}
Analytics Tracking
function processDeliveryEvent(payload) {
// Track delivery metrics using payload wrapper fields
analytics.track('delivery_event', {
event_type: payload.event_type,
event_id: payload.event_id,
order_id: payload.platform_order_id,
reservation_id: payload.reservation_id,
timestamp: payload.event_time_timestamp,
unix_timestamp: payload.event_time_unix_timestamp
});
// Calculate delivery times
if (payload.event_type === 'EVENT_TYPE_PICKUP') {
calculateDeliveryTime(payload.reservation_id);
}
}
Troubleshooting
Webhook Not Received
- Check your endpoint is publicly accessible
- Verify HTTPS is properly configured
- Ensure your server responds with HTTP 200
- Check webhook URL in reservation request
Duplicate Events
- Implement idempotency using
idempotency_keyfrom the payload body (same across retries) - Store processed idempotency keys in database/cache with TTL
- Use
event_idto track individual delivery attempts
Missing Events
- Check server logs for errors
- Verify webhook endpoint uptime
- Contact KioskForce support for webhook delivery logs
Security Best Practices
- Use HTTPS: Always use HTTPS URLs for webhook endpoints (required)
- Verify Signatures: Always verify
X-Webhook-Signaturebefore processing any webhook - Check Timestamps: Reject webhooks with old
X-Webhook-Timestampheader orevent_time_unix_timestamppayload field (e.g., > 5 minutes) to prevent replay attacks - Use Constant-Time Comparison: When verifying signatures, use constant-time comparison functions to prevent timing attacks
- Idempotency: Use
idempotency_keyfrom the payload to handle duplicate deliveries gracefully (same key across retries) - Rotate Keys: Update your webhook API key periodically
- Validate Input: Sanitize and validate all webhook data
- Rate Limiting: Implement rate limiting on webhook endpoints
- Log Events: Log all webhook events with
event_idandidempotency_keyfor debugging and audit
Support
For webhook-related issues:
- 📧 Email: api-support@kioskforce.com
- 📖 Include: Webhook URL, event type, and timestamps
- 🔍 Check: Server logs and network connectivity
Webhook integration enables real-time order tracking and provides the best customer experience with your delivery locker integration!