Juniper sends real-time notifications to your server when order status changes. Configure your webhook URL in Partner Settings to receive these updates automatically.
Set your webhook endpoint using the Partner Settings API:
curl -X PATCH https://api.fulfillment.sandbox.juniperhealth.com/v1/partners/settings \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://your-server.com/webhooks/juniper"
}'When you set a webhook URL for the first time, Juniper automatically generates a webhook secret for signature verification. Retrieve your secret using the dedicated endpoint:
curl -X GET https://api.fulfillment.sandbox.juniperhealth.com/v1/partners/settings/webhook-secret \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"{
"webhookSecret": "8f4b2a1c9e7d3f5a6b8c0d2e4f6a8b0c1d3e5f7a9b1c3d5e7f9a1b3c5d7e9f1a"
}The secret is not included in the general /partners/settings response for security reasons.
All webhooks are signed using HMAC-SHA256. Verify signatures to ensure requests originate from Juniper and haven't been tampered with.
| Header | Description |
|---|---|
X-Juniper-Signature | Signature in format t={timestamp},v1={signature} |
X-Juniper-Webhook-Id | Unique identifier for this webhook delivery |
- Extract the timestamp (
t) and signature (v1) from theX-Juniper-Signatureheader - Construct the signed payload:
{timestamp}.{request_body} - Compute HMAC-SHA256 of the signed payload using your webhook secret
- Compare your computed signature with the
v1value - Reject if timestamp is older than 5 minutes (prevents replay attacks)
import hmac
import hashlib
import time
def verify_webhook_signature(payload: bytes, signature_header: str, secret: str) -> bool:
"""
Verify a Juniper webhook signature.
Args:
payload: Raw request body bytes
signature_header: Value of X-Juniper-Signature header
secret: Your webhook secret (64 hex characters)
Returns:
True if signature is valid, False otherwise
"""
# Parse header: t=timestamp,v1=signature
parts = dict(part.split('=', 1) for part in signature_header.split(','))
timestamp = int(parts['t'])
signature = parts['v1']
# Reject if timestamp is older than 5 minutes
if abs(time.time() - timestamp) > 300:
return False
# Compute expected signature
signed_payload = f"{timestamp}.{payload.decode()}"
expected = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
# Use constant-time comparison
return hmac.compare_digest(signature, expected)
# Flask example
@app.route('/webhooks/juniper', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Juniper-Signature')
if not verify_webhook_signature(request.data, signature, WEBHOOK_SECRET):
return 'Invalid signature', 401
# Process the webhook...
return 'OK', 200const crypto = require('crypto');
function verifyWebhookSignature(payload, signatureHeader, secret) {
// Parse header: t=timestamp,v1=signature
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
);
const timestamp = parseInt(parts.t, 10);
const signature = parts.v1;
// Reject if timestamp is older than 5 minutes
if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
return false;
}
// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Use constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express example
app.post('/webhooks/juniper', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-juniper-signature'];
if (!verifyWebhookSignature(req.body.toString(), signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process the webhook...
res.status(200).send('OK');
});If your webhook secret may have been compromised, rotate it immediately:
curl -X POST https://api.fulfillment.sandbox.juniperhealth.com/v1/partners/settings/rotate-webhook-secret \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"The old secret becomes invalid immediately. Update your webhook handler with the new secret from the response.
When an order status changes, Juniper sends an HTTP POST request to your webhook URL with the following payload structure:
{
"order": {
"orderId": "2vSGym0bH8qVEwCIGlyFoRgJq1A",
"status": "SHIPPED",
"fulfillmentInfo": {
"carrier": "UPS",
"trackingNumber": "1Z9999999999999999",
"trackingStatus": "InTransit",
"trackingUrl": "https://www.ups.com/track?tracknum=1Z9999999999999999",
"checkpoints": [
{
"checkpointTime": "2023-10-02T08:00:00Z",
"location": "Los Angeles, CA, USA",
"message": "Shipment picked up",
"tag": "InTransit"
},
{
"checkpointTime": "2023-10-03T14:00:00Z",
"location": "Chicago, IL, USA",
"message": "Package arrived at facility",
"tag": "InTransit"
}
]
},
"createdAt": "2023-10-01T12:00:00Z",
"updatedAt": "2023-10-05T14:30:00Z"
}
}| Field | Type | Description |
|---|---|---|
order.orderId | string | Unique order identifier |
order.status | string | Current order status (see Status Events) |
order.fulfillmentInfo | object | Shipping details (present when shipped) |
order.fulfillmentInfo.carrier | string | Shipping carrier name (e.g., "UPS", "USPS", "FedEx") |
order.fulfillmentInfo.trackingNumber | string | Carrier tracking number |
order.fulfillmentInfo.trackingStatus | string | Current tracking status |
order.fulfillmentInfo.trackingUrl | string | URL to track the shipment |
order.fulfillmentInfo.checkpoints | array | Tracking checkpoint history |
order.createdAt | string | Order creation timestamp (ISO 8601) |
order.updatedAt | string | Order last update timestamp (ISO 8601) |
| Field | Type | Description |
|---|---|---|
checkpointTime | string | Timestamp of the checkpoint event (ISO 8601) |
location | string | Location of the shipment at this checkpoint |
message | string | Status message from the carrier |
tag | string | Status tag (e.g., "InTransit", "OutForDelivery", "Delivered") |
Webhooks are triggered for the following order status changes:
| Status | Description |
|---|---|
NEW | Order received and validated |
PROCESSING | Order is being prepared |
DISPENSING | Order is being dispensed/filled |
FILLED | Order has been filled |
FULFILLED | Order fulfillment complete |
AWAITING_SHIPMENT | Order is packaged and ready to ship |
SHIPPED | Order has shipped (includes fulfillmentInfo with tracking) |
CANCELLED | Order was cancelled |
- Verify signatures - Always verify the
X-Juniper-Signatureheader to ensure authenticity - Return 2xx quickly - Respond with a 2xx status code within 30 seconds to acknowledge receipt
- Process asynchronously - Queue webhook payloads for background processing
- Handle idempotently - The same event may be sent multiple times; use
orderIdandupdatedAtto deduplicate - Check timestamps - Reject webhooks with timestamps older than 5 minutes to prevent replay attacks
If your endpoint doesn't respond with a 2xx status code, Juniper will retry the webhook:
- Retry attempts: 3
- Retry intervals: 1 minute, 5 minutes, 30 minutes
- Timeout: 30 seconds per attempt
After all retries are exhausted, the webhook is marked as failed. You can retrieve missed updates by polling the Get Order endpoint.