Webhooks
Event types, registration, and verification
Webhooks notify your systems when Partner API events occur, so you can avoid polling for every state change.
Event types
| Event | Typical use | Delivery payload fields |
|---|---|---|
customer_created | Customer provisioning completed | customerID, type, eventID |
customer_deleted | Customer cease completed | customerID, type, eventID |
contract_updated | Contract / plan job completed | customerID, type, eventID |
order_created | Hardware order placed | customerID, orderID, type, eventID |
user_created | User added to customer | customerID, userID, type, eventID |
user_deleted | User removed | customerID, userID, type, eventID |
Example event delivery body:
{
"customerID": "550e8400-e29b-41d4-a716-446655440000",
"type": "customer_created",
"eventID": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}{
"customerID": "550e8400-e29b-41d4-a716-446655440000",
"userID": "660e8400-e29b-41d4-a716-446655440001",
"type": "user_created",
"eventID": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}Register a webhook
You supply the callback URL, the events to subscribe to, and a shared secret (key) used to sign deliveries:
POST /v2/webhooks
Content-Type: application/json
Authorization: Bearer <token>
{
"url": "https://your-app.example/webhooks/plp",
"events": ["customer_created", "contract_updated"],
"key": "<your-shared-secret>"
}Response includes:
| Field | Description |
|---|---|
webhookID | Identifier for later GET/PATCH/DELETE |
url | Your callback URL |
events | Subscribed events |
status | unverified, verified, or error |
key | The secret you provided (echoed back) |
Store
keysecurely — use it to validate that callbacks originate from PhoneLine+. Generate a strong random secret; do not reuse credentials from other systems.
New webhooks start as unverified. PhoneLine+ immediately POSTs a verification challenge to your URL. You must complete verification promptly — unverified webhooks expire after 5 minutes.
Verification (challenge)
After POST /v2/webhooks, PhoneLine+ sends a signed POST to your url with:
POST https://your-app.example/webhooks/plp
Content-Type: application/json
X-Signature: <hmac-sha256-hex>
{
"type": "webhook_verification",
"challenge": "a1b2c3d4e5f6…",
"eventID": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}challenge is a 64-character hex string (32 random bytes).
Required response
Respond with HTTP 2xx and JSON that echoes the same challenge value:
{
"challenge": "a1b2c3d4e5f6…"
}On a matching challenge, status becomes verified and event deliveries begin. On mismatch or error, the webhook stays unverified (and will expire if not verified in time).
If delivery failures accumulate later, status may become error.
Example handler (Node.js)
const crypto = require('crypto');
function verifySignature(rawBody, signatureHeader, key) {
const expected = crypto.createHmac('sha256', key).update(rawBody).digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(signatureHeader || '', 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post('/webhooks/plp', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body; // Buffer — must be the exact bytes received
const signature = req.get('X-Signature');
if (!verifySignature(rawBody, signature, process.env.WEBHOOK_KEY)) {
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(rawBody.toString('utf8'));
if (payload.type === 'webhook_verification') {
return res.status(200).json({ challenge: payload.challenge });
}
// Enqueue payload for async processing, then acknowledge
res.status(200).send();
});Verify the signature against the raw request body before parsing. Frameworks that re-serialize JSON can change whitespace and break HMAC checks.
Signature verification
Every delivery (challenge and events) includes:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Signature | Hex-encoded HMAC-SHA256 of the raw JSON body, keyed with your key |
Compute the expected signature as:
HMAC-SHA256(key, raw_request_body) → hex digestCompare it to the X-Signature header using a constant-time comparison.
Python example:
import hmac
import hashlib
def verify_signature(raw_body: bytes, signature_header: str, key: str) -> bool:
expected = hmac.new(key.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header or "")Manage webhooks
GET /v2/webhooks
GET /v2/webhooks/{webhookID}
PATCH /v2/webhooks/{webhookID}
DELETE /v2/webhooks/{webhookID}Only webhooks registered for Partner API v2 appear in these endpoints.
PATCH can update events and/or key. The callback url cannot be changed — delete and re-create the webhook if the URL must change. Rotating key does not re-run the challenge; update your verifier before or as you PATCH.
Handler recommendations
- Respond with 2xx quickly; process asynchronously in your queue if needed.
- Always verify
X-Signaturewith HMAC-SHA256 before trusting the body. - Treat deliveries as at-least-once — make handlers idempotent using
eventIDor resource IDs. - Retry failures on your side for downstream processing, not by rejecting valid signed callbacks.
- Ensure your endpoint is reachable over HTTPS before calling
POST /v2/webhooks, so the challenge can succeed within the 5-minute window.
Pairing with jobs
| Async API | Webhook |
|---|---|
POST /customers → jobID | customer_created |
PATCH .../contract → jobID | contract_updated |
DELETE /customers/{id} → jobID | customer_deleted |
Use Async jobs when you need SIP details or intermediate status; use webhooks for event-driven integrations.
Updated 22 days ago
