Webhooks

Receiving webhook events from Angage

Overview

Extensions receive webhook events from Angage to react to marketplace and ERP state changes. Webhooks are declared in the manifest under the webhooks array.

Declaring Webhooks

In your manifest.json:

{
    "webhooks": [
        "app.installed",
        "app.uninstalled",
        "invoice.paid",
        "customer.created"
    ]
}

The extension's webhook_url in the developer portal is the endpoint that receives these events.

Lifecycle Events

  • app.installed — Tenant installed your extension
  • app.uninstalled — Tenant removed your extension
  • app.activated — Tenant activated the extension
  • app.deactivated — Tenant deactivated the extension
  • app.scopes_updated — Tenant's granted scopes changed

Business Events

Events driven by ERP activity:

  • invoice.created, invoice.updated, invoice.paid
  • payment.received
  • customer.created, customer.updated
  • order.created, order.updated
  • product.created, product.updated
  • inventory.adjusted

Compliance Webhooks (Required)

To pass pre-review checks, your extension MUST declare:

  • tenant.data_request — Respond with exported tenant data
  • tenant.data_redact — Delete tenant data on request

These are enforced by the automated pre-review check and are required for all submissions.

Webhook Payload

Requests are POSTed to your webhook_url with this structure:

{
    "event": "app.installed",
    "tenant_id": "tenant-abc-123",
    "extension_slug": "your-extension",
    "timestamp": "2026-04-07T10:00:00Z",
    "data": {
        "...": "event-specific fields"
    }
}

Signature Verification

Every webhook includes an X-Webhook-Signature header containing an HMAC-SHA256 digest of the raw request body:

X-Webhook-Signature: sha256=<hex_digest>

PHP verification example:

$signature = $request->header('X-Webhook-Signature');
$payload = $request->getContent(); // raw body — do not re-encode
$expected = 'sha256=' . hash_hmac('sha256', $payload, $webhookSecret);

if (!hash_equals($expected, $signature)) {
    abort(401, 'Invalid signature');
}

Response Expectations

  • Return 2xx within 10 seconds
  • Be idempotent — the same event may be delivered more than once
  • Do not block on downstream work — queue it