ERPNext Slot-Based Delivery Integration
This guide covers integrating ERPNext with Courimax for slot-based delivery management. Both ERPNext and Yango share the same delivery slot templates, ensuring consistent availability across all integrations.
Overview
Courimax manages delivery slots as shared templates across all integrations. When you configure slots in the admin panel, they're immediately available to:
- ERPNext (via webhooks and API)
- Yango (via delivery windows API)
- E-commerce platforms (via public availability API)
This unified approach ensures customers see the same available slots regardless of which channel they use.
Architecture
┌─────────────┐
│ Admin UI │ ──── Configure slot templates
└──────┬──────┘
│
▼
┌─────────────────┐
│ Courimax API │ ──── Shared slot templates
└──────┬──────────┘
│
├──────────────┬──────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ ERPNext │ │ Yango │ │E-commerce│
│(webhooks)│ │(delivery │ │(public │
│ │ │ windows) │ │ API) │
└──────────┘ └──────────┘ └──────────┘
Prerequisites
- Courimax Admin Access: Configure slot templates in the admin panel
- ERPNext Instance: Your ERPNext installation with custom delivery slot fields
- API Key: Obtain from Courimax admin (Integrations → ERPNext)
- Webhook URL: ERPNext endpoint to receive slot updates
Setup Steps
1. Configure Assignment Zones
In Courimax Admin → Assignment Zones:
- Draw delivery zone polygons on the map
- Set Zone Purpose to
slotorboth:slot: Zone only for delivery slotsboth: Zone for both dispatch and slots
- Save the zone
Note: Slot templates can only be assigned to zones with slot or both purpose.
2. Configure Company Slot Policy
In Courimax Admin → Settings → Delivery Slot Policy:
| Setting | Description | Example |
|---|---|---|
| Timezone | Default timezone for slot calculations | Asia/Jerusalem |
| Default Duration | Default slot length (minutes) | 120 |
| Default Cutoff | Booking cutoff (minutes before start) | 60 |
| Vehicle Capacity | Default vehicle mix | [{type: "van", count: 2, maxOrders: 10}] |
3. Create Slot Templates
In Courimax Admin → Delivery Slots:
- Select an assignment zone
- Configure slot details:
- Day of Week: 0 (Sunday) to 6 (Saturday)
- Start Time: HH:mm format (e.g.,
09:00) - End Time OR Duration: Either fixed end time or duration in minutes
- Vehicle Capacity: Number of vehicles and orders per vehicle
- Cutoff: Minutes before slot start when booking closes
Example Configuration:
Zone: North Tel Aviv
Day: Monday (1)
Time: 09:00 - 11:00 (or 09:00 with 120 min duration)
Vehicles: 2 vans × 10 orders each = 20 total capacity
Cutoff: 60 minutes
Seed from Policy: Click "Seed Defaults" to create templates from company policy settings.
4. Provision ERPNext Client
In Courimax Admin → Integrations:
- Click Provision Client
- Select platform:
erpnext - Set Webhook URL: Your ERPNext endpoint (e.g.,
https://erpnext.example.com/api/method/courimax.webhooks.receive) - Copy the API Key (format:
cmx_...)
5. Configure ERPNext Webhook Receiver
Create an ERPNext endpoint to receive webhooks:
# erpnext/courimax_integration/webhooks.py
import frappe
import hmac
import hashlib
@frappe.whitelist(allow_guest=True)
def receive():
# Verify webhook signature
api_key = frappe.request.headers.get('X-API-Key')
signature = frappe.request.headers.get('X-Webhook-Signature')
if not verify_signature(api_key, signature, frappe.request.data):
frappe.throw("Invalid signature", frappe.AuthenticationError)
payload = frappe.request.get_json()
event = payload.get('event')
if event == 'erpnext.slots.updated':
handle_slots_updated(payload['payload'])
elif event == 'erpnext.slot.booked':
handle_slot_booked(payload['payload'])
elif event == 'erpnext.polygon.updated':
handle_polygon_updated(payload['payload'])
return {'status': 'ok'}
def verify_signature(api_key, signature, data):
expected = hmac.new(
api_key.encode(),
data,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
def handle_slots_updated(payload):
"""Update local slot cache when templates change"""
polygon_id = payload['polygon_id']
slots = payload['slots']
availability = payload['availability']
# Update your local cache or display
frappe.db.set_value('Delivery Zone', polygon_id, 'slots_updated', 1)
frappe.db.commit()
def handle_slot_booked(payload):
"""Update slot availability when an order is booked"""
template_id = payload['template_id']
booked = payload['booked']
available = payload['available']
# Update your display
frappe.cache().hset('slot_availability', template_id, {
'booked': booked,
'available': available
})
API Endpoints
Create Order with Slot
Create an order and assign a delivery slot in one request.
Endpoint: POST /api/integrations/erpnext/orders
Headers:
X-API-Key: cmx_your_api_key
Content-Type: application/json
Request Body:
{
"externalRef": "SO-2026-001",
"pickupAddress": "Warehouse A, 123 Main St",
"pickupLat": 32.0853,
"pickupLng": 34.7818,
"dropoffAddress": "Customer Home, 456 Oak Ave",
"dropoffLat": 32.0923,
"dropoffLng": 34.7892,
"recipientName": "John Doe",
"recipientPhone": "+972501234567",
"packageSize": "small",
"deliverySlotTemplateId": "uuid-of-template",
"deliverySlotDate": "2026-08-31"
}
Response:
{
"id": "order-uuid",
"status": "pending",
"externalRef": "SO-2026-001",
"deliverySlotTemplateId": "uuid-of-template",
"deliverySlotDate": "2026-08-31",
"deliveryWindowStart": "2026-08-31T09:00:00+03:00",
"deliveryWindowEnd": "2026-08-31T11:00:00+03:00",
"assignmentZoneId": "zone-uuid",
"trackingToken": "abc123def456"
}
Assign Slot to Existing Order
Assign or reassign a delivery slot to an existing order.
Endpoint: PUT /api/integrations/erpnext/orders/:externalRef/delivery-slot
Headers:
X-API-Key: cmx_your_api_key
Content-Type: application/json
Request Body:
{
"deliverySlotTemplateId": "uuid-of-template",
"deliverySlotDate": "2026-08-31",
"dropoffLat": 32.0923,
"dropoffLng": 34.7892
}
Note: dropoffLat and dropoffLng are optional if the order already has a dropoff address.
Response:
{
"id": "order-uuid",
"deliverySlotTemplateId": "uuid-of-template",
"deliverySlotDate": "2026-08-31",
"deliveryWindowStart": "2026-08-31T09:00:00+03:00",
"deliveryWindowEnd": "2026-08-31T11:00:00+03:00"
}
Get Order by External Reference
Retrieve an order by its ERPNext Sales Order reference.
Endpoint: GET /api/integrations/erpnext/orders/:externalRef
Headers:
X-API-Key: cmx_your_api_key
Response:
{
"id": "order-uuid",
"status": "pending",
"externalRef": "SO-2026-001",
"deliverySlotTemplateId": "uuid-of-template",
"deliverySlotDate": "2026-08-31",
"deliveryWindowStart": "2026-08-31T09:00:00+03:00",
"deliveryWindowEnd": "2026-08-31T11:00:00+03:00",
"assignmentZoneId": "zone-uuid",
"trackingToken": "abc123def456",
"createdAt": "2026-08-30T10:00:00.000Z"
}
Webhook Events
Courimax sends the following webhook events to your ERPNext instance:
erpnext.slots.updated
Triggered when slot templates change (create/update/delete/seed) or when a booking is cancelled.
Payload:
{
"event": "erpnext.slots.updated",
"eventId": "erpnext.slots.updated:zone-uuid:2026-08-30T10",
"payload": {
"polygon_id": "zone-uuid",
"polygon_name": "North Tel Aviv",
"zone_purpose": "slot",
"geodata": {
"type": "Polygon",
"coordinates": [[[34.78, 32.08], [34.79, 32.08], ...]]
},
"slots": [
{
"id": "template-uuid",
"startTime": "09:00",
"endTime": "11:00",
"durationMinutes": 120,
"cutoffMinutes": 60,
"vehicleCapacity": [
{
"vehicleType": "van",
"count": 2,
"maxOrdersPerVehicle": 10
}
],
"capacity": 20
}
],
"availability": [
{
"date": "2026-08-31",
"templateId": "template-uuid",
"capacity": 20,
"booked": 5,
"available": 15
}
]
}
}
erpnext.slot.booked
Triggered when an order is created with a delivery slot.
Payload:
{
"event": "erpnext.slot.booked",
"eventId": "erpnext.slot.booked:order-uuid",
"payload": {
"polygon_id": "zone-uuid",
"polygon_name": "North Tel Aviv",
"order_id": "order-uuid",
"template_id": "template-uuid",
"date": "2026-08-31",
"booked": 6,
"capacity": 20,
"available": 14,
"updatedAt": "2026-08-30T10:05:00.000Z"
}
}
erpnext.polygon.updated
Triggered when an assignment zone polygon is created, updated, or deleted.
Payload:
{
"event": "erpnext.polygon.updated",
"eventId": "erpnext.polygon.updated:zone-uuid:2026-08-30T10",
"payload": {
"polygon_id": "zone-uuid",
"polygon_name": "North Tel Aviv",
"geodata": {
"type": "Polygon",
"coordinates": [[[34.78, 32.08], [34.79, 32.08], ...]]
},
"enabled": true,
"zone_purpose": "slot",
"updatedAt": "2026-08-30T10:00:00.000Z"
}
}
Complete Workflow Example
Scenario: Customer Books a Delivery Slot
-
Customer selects delivery slot in ERPNext
# ERPNext frontend shows available slots
slots = get_available_slots(customer_address, date) -
ERPNext creates order with slot
import requests
response = requests.post(
'https://api.courimax.com/api/integrations/erpnext/orders',
headers={
'X-API-Key': 'cmx_your_api_key',
'Content-Type': 'application/json'
},
json={
'externalRef': 'SO-2026-001',
'pickupAddress': 'Warehouse A, 123 Main St',
'pickupLat': 32.0853,
'pickupLng': 34.7818,
'dropoffAddress': 'Customer Home, 456 Oak Ave',
'dropoffLat': 32.0923,
'dropoffLng': 34.7892,
'recipientName': 'John Doe',
'recipientPhone': '+972501234567',
'packageSize': 'small',
'deliverySlotTemplateId': 'template-uuid',
'deliverySlotDate': '2026-08-31'
}
)
order = response.json() -
Courimax validates and books slot
- Checks slot capacity
- Validates dropoff coordinates are within zone polygon
- Checks booking cutoff time
- Creates order with slot assignment
-
Courimax sends webhook to ERPNext
{
"event": "erpnext.slot.booked",
"payload": {
"order_id": "order-uuid",
"template_id": "template-uuid",
"date": "2026-08-31",
"booked": 6,
"available": 14
}
} -
ERPNext updates local cache
# Update slot availability display
frappe.cache().hset('slot_availability', template_id, {
'booked': 6,
'available': 14
})
Capacity Management
How Capacity Works
- Total Capacity = Σ(vehicle count × max orders per vehicle)
- Booked = Number of active orders assigned to the slot
- Available = Total capacity - Booked
- Cancelled/Failed orders do NOT consume capacity
Example
Slot: Monday 09:00-11:00
Vehicles: 2 vans × 10 orders each
Total Capacity: 20
Orders:
- Order A: booked ✓
- Order B: booked ✓
- Order C: booked ✓
- Order D: cancelled ✗ (doesn't count)
Booked: 3
Available: 17
Concurrency Control
Courimax uses PostgreSQL advisory locks to prevent overbooking:
- Lock is acquired per
(templateId, date)combination - Multiple concurrent bookings are serialized
- First confirmed order wins
Testing
Test Slot Availability
curl -X GET "https://api.courimax.com/api/public/delivery-slots/availability?lat=32.0923&lng=34.7892&date=2026-08-31" \
-H "X-API-Key: cmx_your_api_key"
Test Order Creation
curl -X POST "https://api.courimax.com/api/integrations/erpnext/orders" \
-H "X-API-Key: cmx_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"externalRef": "TEST-001",
"pickupAddress": "Warehouse",
"pickupLat": 32.0853,
"pickupLng": 34.7818,
"dropoffAddress": "Customer",
"dropoffLat": 32.0923,
"dropoffLng": 34.7892,
"recipientName": "Test User",
"recipientPhone": "+972501234567",
"packageSize": "small",
"deliverySlotTemplateId": "template-uuid",
"deliverySlotDate": "2026-08-31"
}'
Verify Webhook Receipt
Check ERPNext logs:
tail -f /path/to/erpnext/logs/webhook.log
Expected output:
[2026-08-30 10:05:00] Received webhook: erpnext.slot.booked
[2026-08-30 10:05:00] Updated slot availability: template-uuid (booked: 6, available: 14)
Error Handling
Common Errors
| Error | Code | Cause | Solution |
|---|---|---|---|
| Invalid API key | 401 | Missing or invalid X-API-Key header | Check API key in Courimax admin |
| Slot not found | 404 | Template ID doesn't exist | Verify template ID from availability API |
| Slot full | 400 | No capacity remaining | Choose different slot or date |
| Outside zone | 400 | Dropoff coordinates not in zone polygon | Verify customer address is within zone |
| Cutoff passed | 400 | Booking cutoff time has passed | Choose later slot or date |
Error Response Format
{
"statusCode": 400,
"error": "Bad Request",
"message": "Delivery slot is full"
}
Best Practices
1. Cache Slot Availability
Cache slot availability locally to reduce API calls:
# Cache for 5 minutes
slots = frappe.cache().get_value('available_slots', generator=fetch_slots)
2. Handle Webhook Retries
Courimax retries failed webhooks with exponential backoff:
- Attempt 1: Immediate
- Attempt 2: 1 minute
- Attempt 3: 5 minutes
- Attempt 4: 30 minutes
- Attempt 5: 2 hours
Make your webhook handler idempotent.
3. Validate Coordinates
Always validate customer coordinates before booking:
# Geocode address if needed
lat, lng = geocode_address(customer_address)
# Verify coordinates are valid
if not (-90 <= lat <= 90 and -180 <= lng <= 180):
raise ValueError("Invalid coordinates")
4. Monitor Webhook Delivery
Check webhook delivery status in Courimax admin:
- Integrations → ERPNext → Webhook Deliveries
- Look for failed deliveries and retry manually if needed
5. Use External References
Always use externalRef to link Courimax orders to ERPNext Sales Orders:
{
"externalRef": "SO-2026-001"
}
This allows you to retrieve orders by ERPNext reference:
GET /api/integrations/erpnext/orders/SO-2026-001
Production Checklist
- Assignment zones configured with
slotorbothpurpose - Slot templates created for all zones and days
- ERPNext client provisioned with API key
- Webhook URL configured and tested
- Webhook receiver endpoint implemented in ERPNext
- Signature verification enabled
- Slot availability caching implemented
- Error handling and retry logic in place
- Monitoring and alerting configured
- Test orders created and verified
Support
For integration issues:
- Check webhook delivery status in Courimax admin
- Review ERPNext webhook receiver logs
- Verify API key and webhook URL configuration
- Test endpoints with curl commands above
For advanced support, contact your Courimax integration specialist.