HTTP reference

Outbound webhooks

Externa pushes domain events to a single HTTPS endpoint you configure in Project settings. Delivery is queued, signed with a project webhook secret (not an API key), and disabled when the URL is empty.

Direction

Outbound webhooks are Externa → your URL. There is no Externa route you POST to in order to “receive” them. Do not confuse this with the AI collection-import webhook (POST /ai/webhooks/collection-import), which is an inbound token-auth endpoint.

Configure

  1. Admin → SettingsProject
  2. Set Webhook URL (e.g. https://example.com/webhooks/externa or a temporary webhook.site URL)
  3. Generate secret (or paste your own) and save — copy the secret into your receiver; Externa encrypts it at rest and never re-shows it in Inertia props
  4. Optionally Send test event (type: ping) — requires a queue worker

Leave the URL empty to disable outbound events. Empty secret on save keeps the existing secret.

Queue worker required

Delivery uses DeliverOutboundWebhookJob. Run a queue worker (composer run dev locally, or php artisan queue:work in production). See Operations.

Envelope

{
  "id": "evt_01h…",
  "type": "item.updated",
  "created_at": "2026-07-24T12:00:00Z",
  "data": {
    "collection_id": 1,
    "collection_slug": "posts",
    "item_id": 42
  }
}

Bodies are minimal (ids / slug only). Full record dumps are out of scope for v1. Empty data is encoded as {} (object), not [].

Event types (v1)

TypeWhen
item.created / item.updatedAfter item field sync
item.deletedSoft or force delete (same event type — consumers cannot distinguish)
item.restoredItem restore
file.created / file.updated / file.deletedFile manager create/update/delete (no file.restored)
collection.created / collection.updated / collection.deletedCollection lifecycle (no collection.restored; soft/force both emit deleted)
pingSettings “Send test event”

Restore coverage

Only items emit *.restored. File restore (FileService::restore) and collection restore do not dispatch outbound webhook events.

Bulk import

CSV / remote JSON imports wrap row writes in OutboundWebhookDispatcher::withoutWebhooks() so a large import does not flood your endpoint.

Headers

HeaderValue
Content-Typeapplication/json
X-Externa-Signaturesha256=<hmac_sha256(raw_body, secret)>
X-Externa-Event-IdSame as payload id
X-Externa-TimestampUnix seconds when the job ran
User-AgentExterna-Webhooks/1.0

Algorithm: HMAC-SHA256 over the exact raw JSON body bytes, using the project webhook secret. Prefix the hex digest with sha256=.

Timeouts: connect 3s, total 5s. Failed deliveries retry (3 tries, backoff 10s / 30s / 60s). Exhausted failures are logged; they do not break the admin/API write path.

Verify HMAC (on your receiver)

Sign the raw request body (not a re-encoded JSON object). Compare with a constant-time equality check.

Node

import crypto from 'node:crypto'

function verify(rawBody, secret, signatureHeader) {
  const expected =
    'sha256=' +
    crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
  const a = Buffer.from(expected)
  const b = Buffer.from(signatureHeader || '')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

PHP

$expected = 'sha256='.hash_hmac('sha256', $rawBody, $secret);
hash_equals($expected, $signatureHeader);

Try / test

  1. Open webhook.site (or run a local receiver) and copy its unique URL
  2. Admin → SettingsProject → paste as Webhook URL, generate/save secret, keep a copy of the secret
  3. Start a queue worker
  4. Click Send test event, or create/update an item in the admin
  5. Inspect the POST on webhook.site: JSON envelope + X-Externa-* headers
  6. Point the URL at your real receiver and verify HMAC with the snippets above

Practice signing with Bruno (optional)

The externa-bruno collection includes PublicApi/Webhooks/Verify Webhook Signature.

Not an Externa route

That request is a docs helper / sample payload, not a live Externa API. Its default URL is {{base_url}}/__webhook_receiver_docs_only__ — an intentional placeholder. Sending it against externa-core 404s. Replace the URL with your receiver (or webhook.site) if you want to POST the sample body yourself.

Use it to:

  • See the envelope shape and header names Externa sends
  • Compute sha256=<hmac> locally (same secret as Project settings → webhook_secret / EXTERNA_WEBHOOK_SECRET in Bruno .env)
  • Set X-Externa-Signature on the request and POST to your receiver to exercise verification code

Bruno is acting as a client toward your webhook endpoint, not as a caller of Externa’s Public API.

Full Bruno notes: externa-bruno README — Webhooks helper.

vs inbound API keys

Outbound webhooksPublic CMS API keys
DirectionExterna → your URLYour app → Externa /api/v1
SecretProject webhook_secret (encrypted)ek_… key (hashed at rest)
AuthHMAC on bodyAuthorization: Bearer

Do not reuse an API key as the webhook signing secret.

Previous
AI API & webhooks