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.