PinDelivery supports client-configurable webhooks that fire automatically on every package status change. When a package intent is executed, the system asynchronously calls the client's configured webhook URL with package and status data.
Clients configure webhooks in the Client Portal under Settings > Profile.
| Field | Description |
|---|---|
| URL | Webhook endpoint. Supports template variables. |
| Method | HTTP method: GET, POST, PUT, or PATCH |
| Headers | Custom headers as JSON (e.g., {"Authorization": "Bearer ..."}) |
| Body | Request body template (for POST/PUT/PATCH). Supports template variables. |
| URL Encode | When checked, template values in the URL are URL-encoded (recommended for GET requests with query parameters) |
Available in URL, Body, and Header values:
| Variable | Description | Example |
|---|---|---|
{{packageID}} |
MongoDB ObjectID | 507f1f77bcf86cd799439011 |
{{readableID}} |
Human-readable ID | PD40000000 |
{{trackingNumber}} |
Tracking number | PD-2026-001234 |
{{vendorCode}} |
Vendor code | PD001234 |
{{status}} |
New status (Serbian) | PAKET DOSTAVLJEN |
{{subStatus}} |
Sub-status detail | Primio komšija |
{{timestamp}} |
Unix timestamp (ms) | 1776242979000 |
{{receiverName}} |
Receiver first name | Marko |
{{receiverLastname}} |
Receiver last name | Petrovic |
{{receiverPhone}} |
Receiver phone | +381641234567 |
{{senderName}} |
Sender/client name | DNS Fashion |
When the "URL Encode" checkbox is enabled, template values substituted into the URL are URL-encoded. This is important for GET requests where values are in query parameters, as status values like PAKET U PROCESU DOSTAVLJANJA contain spaces.
Without URL encoding:
https://example.com/webhook?status=PAKET U PROCESU DOSTAVLJANJA
With URL encoding:
https://example.com/webhook?status=PAKET+U+PROCESU+DOSTAVLJANJA
Body and header values are never URL-encoded regardless of this setting.
URL: https://example.com/webhook?packageId={{packageID}}&status={{status}}
Method: GET
URL Encode: ON
URL: https://example.com/webhook
Method: POST
Body: {"id": "{{packageID}}", "status": "{{status}}", "tracking": "{{trackingNumber}}"}
URL: https://example.com/webhook
Method: POST
Headers: {"Authorization": "Bearer my-secret-token"}
Body: {"packageID": "{{packageID}}", "readableID": "{{readableID}}", "status": "{{status}}"}
If no body template is configured (and method is not GET), a default JSON payload is sent:
{
"packageID": "...",
"readableID": "...",
"trackingNumber": "...",
"vendorCode": "...",
"status": "...",
"subStatus": "...",
"timestamp": 1776242979000,
"receiverName": "...",
"receiverLastname": "...",
"receiverPhone": "...",
"senderName": "..."
}
IntentV2Service.executeForPackage()fireClientWebhook() is called asynchronously (in a goroutine)package.SenderIDURLEncode flag is set[Webhook] prefixPATCH /auth/client/webhook?clientID={id}
Authorization: Bearer <jwt>
Content-Type: application/json
{
"url": "https://example.com/webhook",
"method": "POST",
"headers": {"Authorization": "Bearer ..."},
"body": "{\"id\": \"{{packageID}}\"}",
"urlEncode": true
}
Check webhook activity in backend logs:
journalctl -u pindelivery-dev --since "1 hour ago" | grep -i webhook
Log format:
[Webhook] Fired for client <name> package <readableID> -> <url> (status <httpCode>)
[Webhook] Error calling webhook for client <name> (url=<url>): <error>
Webhook config is stored in the Clients collection under the Webhook field:
db.Clients.findOne({Name: "Client Name"}, {Webhook: 1})
Email and SMS notifications are currently disabled per owner request. Only webhooks are active. The disabled notification code is commented out (not deleted) in:
IntentV2Service.goCourierService.goPackageService.goPShopService.goCronService.go