Skip to content

Webhooks Overview

Webhooks are how Pipelet pushes OCPP events to your application in real time. Subscribe once with the Headless API, then receive HTTP POSTs whenever a StartTransaction, MeterValues, StatusNotification, etc. arrives.

23 event types are supported — see the Event Reference for the full list.

1. Create → POST /api/v1/webhooks
2. Test → POST /api/v1/webhooks/{id}/test (synthetic event for verification)
3. Receive → Pipelet POSTs to your URL whenever a subscribed event fires
4. Update → PUT /api/v1/webhooks/{id} (change events, URL, or secret)
5. Delete → DELETE /api/v1/webhooks/{id}
Terminal window
curl -X POST http://localhost:8080/api/v1/webhooks \
-H "X-API-Key: hcpms_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "My Integration",
"url": "https://my-app.example.com/webhooks/ocpp",
"enabled": true,
"events": [
"StartTransaction",
"StopTransaction",
"StatusNotification",
"MeterValues"
],
"secret_header": "X-Webhook-Secret",
"secret_value": "my-secret-value-here"
}'

The response includes a numeric id you can use to update or delete the webhook later.

Every delivery is a JSON POST with this shape:

{
"event": "StartTransaction",
"station_id": "WALLBOX_001",
"timestamp": "2026-04-14T12:34:56.789Z",
"payload": {
"connectorId": 1,
"idTag": "RFID_12345678",
"meterStart": 12345,
"timestamp": "2026-04-14T12:34:56Z",
"transactionId": 42
}
}

The payload field contains the raw OCPP message body — schemas are defined by the OCPP 1.6 spec.

Pipelet authenticates outbound webhooks with a shared secret sent as an HTTP header. You configure the header name (secret_header) and value (secret_value) when creating the webhook.

The simplest receiver pattern:

from flask import Flask, request, abort
app = Flask(__name__)
EXPECTED_SECRET = "my-secret-value-here"
@app.post("/webhooks/ocpp")
def receive():
if request.headers.get("X-Webhook-Secret") != EXPECTED_SECRET:
abort(401)
event = request.json
print(f"{event['event']} from {event['station_id']}")
return "", 200
PropertyValue
Delivery guaranteeAt-least-once
Successful responseHTTP 2xx within 10 seconds
Retry on failureYes — exponential backoff
Backoff schedule30s, 1m, 5m, 30m, 2h, 6h, 24h
Max attempts7 (then dead-lettered)
Dead-letteringFailed deliveries land in failed_deliveries table
OrderBest-effort chronological — not strictly ordered across stations

At-least-once means your handler must be idempotent. Use transactionId or (station_id, payload.timestamp) as a dedupe key.

Pipelet provides a built-in test endpoint that POSTs a synthetic payload to your webhook URL:

Terminal window
curl -X POST http://localhost:8080/api/v1/webhooks/42/test \
-H "X-API-Key: hcpms_live_..."

Response includes the HTTP status your endpoint returned:

{
"ok": true,
"status_code": 200,
"duration_ms": 124
}

This is invaluable for staging — you can verify your receiver is reachable, your secret is correct, and your handler doesn’t 500 on the canonical payload shape before any real charger sends data.

SymptomCauseFix
Webhooks fire but receiver never sees themFirewall/NAT in the wayUse a tunnel like ngrok during development
401 from your endpointSecret mismatchCheck both header name and value — both are case-sensitive
Duplicate eventsYour endpoint returned non-2xx, Pipelet retriedMake handler idempotent; return 2xx fast even before processing
Order-dependent logic brokenYou assumed strict orderingLook at payload.timestamp, not arrival order
Receiver overwhelmed during reconnect stormsAll stations re-sending state at onceBatch + debounce on your side, or filter to only the events you need