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

EventTypical useDelivery payload fields
customer_createdCustomer provisioning completedcustomerID, type, eventID
customer_deletedCustomer cease completedcustomerID, type, eventID
contract_updatedContract / plan job completedcustomerID, type, eventID
order_createdHardware order placedcustomerID, orderID, type, eventID
user_createdUser added to customercustomerID, userID, type, eventID
user_deletedUser removedcustomerID, 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:

FieldDescription
webhookIDIdentifier for later GET/PATCH/DELETE
urlYour callback URL
eventsSubscribed events
statusunverified, verified, or error
keyThe secret you provided (echoed back)

Store key securely — 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:

HeaderValue
Content-Typeapplication/json
X-SignatureHex-encoded HMAC-SHA256 of the raw JSON body, keyed with your key

Compute the expected signature as:

HMAC-SHA256(key, raw_request_body) → hex digest

Compare 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-Signature with HMAC-SHA256 before trusting the body.
  • Treat deliveries as at-least-once — make handlers idempotent using eventID or 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 APIWebhook
POST /customersjobIDcustomer_created
PATCH .../contractjobIDcontract_updated
DELETE /customers/{id}jobIDcustomer_deleted

Use Async jobs when you need SIP details or intermediate status; use webhooks for event-driven integrations.


Did this page help you?