Skip to main content

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​

  1. Courimax Admin Access: Configure slot templates in the admin panel
  2. ERPNext Instance: Your ERPNext installation with custom delivery slot fields
  3. API Key: Obtain from Courimax admin (Integrations → ERPNext)
  4. Webhook URL: ERPNext endpoint to receive slot updates

Setup Steps​

1. Configure Assignment Zones​

In Courimax Admin → Assignment Zones:

  1. Draw delivery zone polygons on the map
  2. Set Zone Purpose to slot or both:
    • slot: Zone only for delivery slots
    • both: Zone for both dispatch and slots
  3. 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:

SettingDescriptionExample
TimezoneDefault timezone for slot calculationsAsia/Jerusalem
Default DurationDefault slot length (minutes)120
Default CutoffBooking cutoff (minutes before start)60
Vehicle CapacityDefault vehicle mix[{type: "van", count: 2, maxOrders: 10}]

3. Create Slot Templates​

In Courimax Admin → Delivery Slots:

  1. Select an assignment zone
  2. 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:

  1. Click Provision Client
  2. Select platform: erpnext
  3. Set Webhook URL: Your ERPNext endpoint (e.g., https://erpnext.example.com/api/method/courimax.webhooks.receive)
  4. 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​

  1. Customer selects delivery slot in ERPNext

    # ERPNext frontend shows available slots
    slots = get_available_slots(customer_address, date)
  2. 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()
  3. Courimax validates and books slot

    • Checks slot capacity
    • Validates dropoff coordinates are within zone polygon
    • Checks booking cutoff time
    • Creates order with slot assignment
  4. 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
    }
    }
  5. 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​

ErrorCodeCauseSolution
Invalid API key401Missing or invalid X-API-Key headerCheck API key in Courimax admin
Slot not found404Template ID doesn't existVerify template ID from availability API
Slot full400No capacity remainingChoose different slot or date
Outside zone400Dropoff coordinates not in zone polygonVerify customer address is within zone
Cutoff passed400Booking cutoff time has passedChoose 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 slot or both purpose
  • 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:

  1. Check webhook delivery status in Courimax admin
  2. Review ERPNext webhook receiver logs
  3. Verify API key and webhook URL configuration
  4. Test endpoints with curl commands above

For advanced support, contact your Courimax integration specialist.