Skip to content

Webhooks

Beta

Pennylane webhooks are in beta. Pennylane itself recommends keeping the changelog endpoints as a fallback while the feature stabilizes. Known event types so far: customer_invoice.e_invoicing_status_updated and dms_file.created.

Subscribe

<?php

use Pennylane\Sdk\Pennylane;

$client = new Pennylane(); // reads PENNYLANE_API_TOKEN

$subscription = $client->webhookSubscriptions->create(
    callbackUrl: 'https://example.com/webhooks/pennylane', // must be HTTPS
    events: ['customer_invoice.e_invoicing_status_updated'],
);
echo $subscription->secret;

Store the secret now

The secret used to verify deliveries is returned only once, at creation. Store it in your secret manager immediately; you cannot fetch it again.

Manage subscriptions with list(), get($id), update($id, ...) and delete($id).

Receive and verify deliveries

Pennylane signs deliveries with an HMAC of the raw body using your secret. Verify before trusting:

<?php

use Pennylane\Sdk\Webhooks\Webhooks;
use Pennylane\Sdk\Exception\WebhookSignatureException;

$secret = getenv('PENNYLANE_WEBHOOK_SECRET'); // from your secret manager

$raw = file_get_contents('php://input'); // the RAW body, do not re-serialize
$signature = $_SERVER['HTTP_X_PENNYLANE_SIGNATURE'] ?? '';

try {
    $event = Webhooks::parseEvent($raw, signature: $signature, secret: $secret);
} catch (WebhookSignatureException $e) {
    http_response_code(400);
    exit('bad signature');
}

if ($event->event === 'customer_invoice.e_invoicing_status_updated') {
    // ...
}

http_response_code(200);
echo json_encode(['ok' => true]);

Signature header

As of mid-2026 Pennylane does not document the exact signature header name or encoding. Webhooks::verifySignature() is deliberately tolerant: it accepts hex and base64 HMAC-SHA256 encodings, with or without a sha256= or hmac-sha256= prefix, compared in constant time. Inspect your first real deliveries to confirm the header (commonly X-Pennylane-Signature) and pin your integration accordingly.

You can also verify manually:

$isValid = Webhooks::verifySignature($rawBody, $signatureHeader, $secret);

Fallback: changelogs

For guaranteed-delivery synchronization, poll the changelog endpoints. They return change events per resource, cursor-paginated, so an incremental sync loop is trivial:

foreach ($client->changelogs->customerInvoices() as $event) {
    // each event references the changed invoice
}

Changelogs exist for customer invoices, supplier invoices, customers, suppliers, products, ledger entry lines, transactions and quotes.