This repository is sample integration code showing how to connect Extensiv to Voodoo Robotics devices and automatically turn Extensiv order events into commands for a Voodoo-powered workflow. The code is intentionally over-commented so it can be copied, modified, and adapted to real deployments.
Receives HTTPS webhook POST requests from Extensiv's 3PL Warehouse Manager, parses order events, extracts a pick list (SKU, quantity, location, lot) from each order's allocations, and forwards orders to the Voodoo Robotics API so Voodoo Robotics devices can act on those instructions.
Voodoo Robotics: https://voodoorobotics.com/
| File | Purpose |
|---|---|
server.py |
Main integration service — receives Extensiv webhooks, validates them, extracts picks, and syncs orders to Voodoo. |
basicExtensivReceiver.py |
Demo/reference — an earlier, simpler version that just prints raw headers and the Extensiv payload envelope. Kept as a learning reference. |
exampleOrder.json |
A real-world Extensiv webhook payload captured from the sandbox. Use this to understand the JSON structure without needing a live connection. |
tasks.json |
Maps Extensiv event types to ordered actions so the integration can automate the Voodoo side without hard-coding behavior in Python. |
.env |
Your local configuration (port, certificate paths, log file, API keys). Never committed to git. |
.env.example |
Template showing all available settings. Copy this to .env to get started. |
LICENSE |
MIT license granting broad permission to use, copy, modify, and redistribute this code. |
pip install -r requirements.txtThis repository demonstrates the full integration path:
- Extensiv sends webhook events over HTTPS.
- The server validates the webhook source and signature.
- The payload is translated into picks and order actions.
- Those actions are sent to the Voodoo Robotics API.
- Voodoo Robotics devices can then respond to the resulting workflow.
This is not just a generic Extensiv webhook example. It is a working reference for connecting Extensiv operations to Voodoo Robotics device automation.
Copy .env.example to .env and set:
| Variable | Description | Default |
|---|---|---|
SERVER_PORT |
Port to listen on | 8443 |
TLS_CERT_FILE |
Path to TLS fullchain file | — |
TLS_KEY_FILE |
Path to TLS private key file | — |
LOG_FILE |
Path to the log file | webhook.log |
LOG_LEVEL |
Logging threshold: DEBUG, INFO, WARNING, ERROR, CRITICAL |
INFO |
VOODOO_API_ENDPOINT |
Base URL of the Voodoo Orders API | — |
VOODOO_API_KEY |
API key for Voodoo authentication | — |
VOODOO_API_TIMEOUT_SECONDS |
Timeout for outbound Voodoo API calls | 30 |
TASKS_FILE |
Path to JSON file with event actions | tasks.json |
TIGHT_SECURITY |
Require HTTPS and Extensiv signatures | true |
EXTENSIV_PUBLIC_KEY_CACHE_FILE |
JSON cache for Extensiv key endpoint response | extensiv_public_key_cache.json |
Task routing is configured in tasks.json, not inline in .env.
Example:
{
"OrderPickJobUserAssigned": ["DELETE", "ADD"],
"OrderPickJobUserUnassigned": ["DELETE", "ADD"],
"OrderExcludedFromPickJob": ["DELETE"]
}tasks.json maps each Extensiv webhook event type to an ordered list of actions for Voodoo. The server looks up the incoming event name, then runs each action in the listed order.
Format:
{
"ExtensivEventName": ["ACTION_ONE", "ACTION_TWO"]
}Rules:
- Each top-level key is an Extensiv event type such as
OrderPickJobUserAssigned - Each value is an array of action names
- Actions are executed in order, so
["DELETE", "ADD"]behaves differently from["ADD", "DELETE"] - Action names are normalized to uppercase at runtime, but uppercase values are recommended for readability
- Events not listed in
tasks.jsonare logged and ignored - Unknown action names are logged as warnings and skipped
| Action | What it does | Voodoo API call | Notes |
|---|---|---|---|
DELETE |
Removes the order from Voodoo | DELETE /api/order/{order_id}/ |
Commonly used before ADD so the latest order data replaces any older copy. If the order does not exist yet, the failure is logged as a warning and processing continues. |
ADD |
Creates or re-creates the order in Voodoo using picks extracted from the Extensiv payload | POST /api/order/ |
Sends order_number plus shipments[], using Extensiv routingInfo.trackingNumber as shipment_number when present. |
LAUNCH |
Launches an existing Voodoo order | POST /api/order/{order_id}/launch/ |
Use this after ADD if your Voodoo workflow should start immediately after creation. |
ABORT |
Aborts an existing Voodoo order | POST /api/order/{order_id}/abort/ |
Useful when an Extensiv event means work should stop for an order already in progress. |
{
"OrderPickJobUserAssigned": ["DELETE", "ADD"],
"OrderPickJobUserUnassigned": ["DELETE", "ADD"],
"OrderExcludedFromPickJob": ["DELETE"]
}What these do:
["DELETE", "ADD"]: replace the current Voodoo order with a fresh version built from the newest Extensiv payload["DELETE"]: remove the order from Voodoo and do not recreate it
You can also use sequences such as ["ADD", "LAUNCH"] or ["ABORT", "DELETE"] if that matches your Voodoo workflow. The important detail is that the array order is the execution order.
In Extensiv 3PL Manager, open Customers > Event Notifications and create a webhook with the following recommended settings:
Webhook Scope:Any CustomerDescription:Voodoo IntegrationDestination URL:https://your.domain.com:8443/Event Type:OrderEvents:Excluded from pickjob,Pick job - user assigned,Pick job - user unassignedInclude Resources: enableInclude resource in payloadResource Options: enableOrder custom fields,Packages,Order items,Contacts,Billing details,Outbound serial numbers,Small parcel,Allocations,Allocations with detail, andParcel options
Replace your.domain.com with the public hostname on your TLS certificate. If you change SERVER_PORT in .env, use that same port in the webhook URL.
server.py executes the actions listed for each event type in tasks.json and ignores events that are not configured there.
| Event type | Action |
|---|---|
OrderPickJobUserAssigned |
Delete, then add |
OrderPickJobUserUnassigned |
Delete, then add |
OrderExcludedFromPickJob |
Delete the order from Voodoo |
When TIGHT_SECURITY=true, the server refreshes Extensiv's webhook verification key during startup.
Reference: https://secure-wms.com/events/webhook/key
Startup behavior:
- The server checks
EXTENSIV_PUBLIC_KEY_CACHE_FILEfor the last saved key response. - If a cached
retrievalDateISOexists, the server sends a request tohttps://secure-wms.com/events/webhook/keywithpreviousRetrievalDateISOto check whether the key has rotated. - If Extensiv returns
304 Not Modified, the cachedpublicKeyis reused. - If Extensiv returns a fresh payload, the new
publicKeyandretrievalDateISOare saved back to the cache file. - If the refresh request fails, the server falls back to the cached key.
- If no valid public key is available, startup fails when
TIGHT_SECURITYis enabled.
The cache file stores Extensiv's response structure directly:
{
"publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
"retrievalDateISO": "2026-03-22T17:23:38.895Z"
}Example .env settings:
LOG_LEVEL=INFO
TIGHT_SECURITY=true
EXTENSIV_PUBLIC_KEY_CACHE_FILE=extensiv_public_key_cache.jsonThis follows Extensiv's documented key retrieval and rotation model using the cache file as the sole persisted key state.
Extensiv will not connect to a server using a self-signed certificate. The webhook sender validates the server's certificate against a trusted CA chain. You must use a certificate issued by a public CA.
certbot is the standard tool for obtaining Let's Encrypt certificates. Your server needs a public domain name pointing to its IP, and port 80 must be reachable for the HTTP-01 challenge.
# Install certbot (Ubuntu/Debian)
sudo apt-get install certbot
# Obtain a certificate (stops any existing service on port 80 briefly)
sudo certbot certonly --standalone -d your.domain.com
# Certificates are written to:
# /etc/letsencrypt/live/your.domain.com/fullchain.pem ← TLS_CERT_FILE
# /etc/letsencrypt/live/your.domain.com/privkey.pem ← TLS_KEY_FILESet your .env accordingly:
SERVER_PORT=8443
TLS_CERT_FILE=/etc/letsencrypt/live/your.domain.com/fullchain.pem
TLS_KEY_FILE=/etc/letsencrypt/live/your.domain.com/privkey.pem
LOG_LEVEL=INFOImportant: Use
fullchain.pem, notcert.pem. The full chain includes the intermediate CA certificates that clients need to verify trust. Usingcert.pemalone will cause TLS handshake failures.
Let's Encrypt certificates expire after 90 days. Set up automatic renewal:
# Test renewal (dry run)
sudo certbot renew --dry-run
# Certbot installs a systemd timer or cron job automatically on most systems.
# Verify it is active:
sudo systemctl status certbot.timerRenewal does not reach a running server.
server.pyreadsTLS_CERT_FILEandTLS_KEY_FILEonce at startup and holds that certificate in memory for the life of the process. certbot's renewal only replaces the files on disk; the running server keeps presenting the old certificate until it is restarted. Once that old certificate expires, Extensiv's TLS handshakes fail even thoughcertbot certificatesreports a valid, renewed certificate.Whatever supervises the server must restart it after every renewal. With systemd this is a certbot deploy hook — see step 5 of the systemd setup, which is a required part of the installation, not an optional extra.
For a persistent deployment, install the server as a systemd service so it starts automatically on boot and restarts after failures.
All five steps are required. Step 5 installs a certbot deploy hook that restarts the service after each certificate renewal. The server loads its TLS certificate once at startup, so a deployment that stops at step 4 will keep running but silently stop accepting Extensiv webhooks about 90 days after the certificate was issued — when the certificate it loaded expires, regardless of what certbot has since renewed on disk.
sudo nano /etc/systemd/system/extensiv-receiver.servicePaste the following, adjusting WorkingDirectory and ExecStart to your
actual installation path:
[Unit]
Description=Extensiv Webhook Receiver
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/home/ubuntu/voodoo-extensiv
ExecStart=/usr/bin/python3 /home/ubuntu/voodoo-extensiv/server.py
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.targetNote:
User=rootis required if the TLS private key under/etc/letsencrypt/live/is root-only readable (the default for Let's Encrypt certificates). If you relax those permissions or copy the key to a location readable by a less-privileged user, changeUser=accordingly.
# Reload systemd so it picks up the new unit file
sudo systemctl daemon-reload
# Enable the service to start automatically at boot
sudo systemctl enable extensiv-receiver
# Start it now
sudo systemctl start extensiv-receiver# Service status (running / failed / restart count, etc.)
sudo systemctl status extensiv-receiver
# Live log tail
sudo journalctl -u extensiv-receiver -f
# Last 100 lines
sudo journalctl -u extensiv-receiver -n 100.env, tasks.json, and the TLS certificate files are all read once at
startup, so any change to them requires a restart:
sudo systemctl restart extensiv-receiverserver.py calls load_cert_chain() once in main() and keeps the result in
memory for the life of the process. certbot's renewal timer only replaces the
files under /etc/letsencrypt/live/; the running service keeps presenting the
certificate it loaded at startup. When that certificate reaches its expiry
date, Extensiv's deliveries start failing with TLS handshake errors while
certbot certificates still shows a valid, renewed certificate on disk.
Install a certbot deploy hook so every successful renewal restarts the service:
sudo bash -c 'cat > /etc/letsencrypt/renewal-hooks/deploy/restart-extensiv-receiver.sh << "EOF"
#!/bin/sh
systemctl restart extensiv-receiver
EOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/restart-extensiv-receiver.sh'Deploy hooks run only after a renewal actually succeeds, so the routine
twice-daily certbot.timer checks do not restart the service. If your unit
file uses a different name, change extensiv-receiver in the hook to match;
certbot does not verify that the service named in a hook exists.
Confirm the hook is installed and executable:
ls -l /etc/letsencrypt/renewal-hooks/deploy/Compare the expiry date the service is presenting with the one on disk. The
two should match. If the served date is earlier, the service is still running
on the pre-renewal certificate and needs sudo systemctl restart extensiv-receiver.
# Certificate the running service is presenting
echo | openssl s_client -connect localhost:8443 2>/dev/null | openssl x509 -noout -enddate
# Certificate on disk
sudo openssl x509 -in /etc/letsencrypt/live/your.domain.com/fullchain.pem -noout -enddateThe receiver looks healthy — systemctl status shows it running, it is
listening on its port, and the log simply goes quiet — but the customer
reports that orders have stopped arriving in Voodoo. From another machine,
curl -v https://your.domain.com:8443/ fails with certificate has expired,
and the log shows TLS handshake failed entries beginning at the old
certificate's expiry time.
To recover:
sudo systemctl restart extensiv-receiver, then confirm with theopensslcomparison above.- Check the webhook in Extensiv (
Customers > Event Notifications). After a run of failed deliveries it may have been disabled and need re-enabling. - Ask the customer to re-trigger the pick-job events that occurred while the certificate was expired; those deliveries were rejected at the TLS layer and never reached the server or its log.
- Install the deploy hook above if it is missing.
# The server needs root to read Let's Encrypt certificate files
sudo python server.pyWhen an order webhook arrives, the console shows:
============================================================
ORDER ID: 24422720
EVENT: OrderPickJobUserAssigned
PICKS: 2 allocation(s)
------------------------------------------------------------
Pick 1:
SKU : RIT0304
Quantity : 1
Location : 03-19-01
Lot : (none)
Pick 2:
SKU : BA04050
Quantity : 1
Location : NOR-06-01
Lot : DO0824USA0004
============================================================
All log messages (including those from non-order events, errors, and TLS
handshake failures) are also written to the log file specified by LOG_FILE.
This repository is released under the MIT License. See LICENSE.
That means you can:
- Copy the code.
- Modify it.
- Use it internally or commercially.
- Redistribute it.
- Build your own version on top of it.
This project is provided as reusable sample integration code. No claim is made that you must keep your changes private or ask for permission to adapt it.