Outgoing webhooks — how to integrate and extend
This guide explains how Tenantx outgoing webhooks fit real integrations (CRM, inventory, hospital-style workflows), how organization-wide vs workspace endpoints behave, and how you extend the kernel with your own events.
API and payload reference: OUTGOING_WEBHOOKS.md
Availability by plan
Outgoing webhooks are a subscription-gated kernel feature:
trial,starter, andpro: not availableenterprise: available
If an organization is on a plan without the feature, the tenant API returns 402 Payment Required with feature_key: outgoing_webhooks, and the org-admin UI should hide webhook navigation automatically when it uses the shared permission+feature guards.
1. Mental model
- Tenantx → your server: HTTP POST, JSON body, optional HMAC on the raw body.
- Not a pull API: your CRM does not poll Tenantx for these notifications; Tenantx pushes when something happens (or when your code calls the dispatcher after business logic runs).
- Queues: delivery is queued. You must run Laravel’s queue worker; otherwise jobs sit in
jobs(or your Redis driver) until processed. - Retries: failed HTTP attempts log warnings and can use Laravel’s failed job / retry behavior depending on your queue config.
Use the delivery envelope field id (X-TenantX-Delivery) as an idempotency key on your side so duplicate deliveries (retries) do not create duplicate rows in the CRM.
2. Organization-wide vs workspace-scoped endpoints
When creating a webhook in Organization admin → Webhooks, you choose:
| Scope | workspace_id in API | Who receives deliveries |
|---|---|---|
| Entire organization | null | Hooks that only match organization-level dispatches (fourth argument to the dispatcher is null), and any dispatch that includes a workspace id (org-wide hooks still get workspace-tagged events). |
| Specific workspace | UUID of workspace_branding | Only when the dispatcher is called with that workspace id; not for pure org-only events. |
Today’s kernel events (organization.registered, invitation.accepted, subscription.updated) are dispatched without a workspace id. So:
- Organization-wide endpoints do receive them.
- Workspace-scoped endpoints do not receive them until you add code that dispatches with a workspace id, or you add new events that pass one.
That is intentional: workspace URLs are ready for per-branch / per-site / per-department traffic without mixing org-level billing events into every branch endpoint.
3. How this works with a CRM (example: “empty inventory” → notify on receipt)
Tenantx does not ship inventory tables or inventory.low events in the kernel. A buyer (or your product built on Tenantx) adds:
- Domain tables (e.g.
inventory_itemswithorganization_id,workspace_id). - Application logic when stock changes — e.g. after a goods receipt is posted or when quantity crosses zero.
At that point you call the outgoing dispatcher from your controller or service:
use App\Services\Webhooks\OutgoingWebhookDispatcher;
use App\Support\OutgoingWebhookEvent; // or your own event name constant
// After you detect empty (or below threshold) for this workspace:
app(OutgoingWebhookDispatcher::class)->dispatch(
$organizationId,
'inventory.low', // add to OutgoingWebhookEvent + validation Rule::in(...) if you use strict API validation
[
'sku' => $sku,
'quantity' => $qty,
'location_code' => $locationCode,
'trigger' => 'goods_receipt_posted',
],
$workspaceId // non-null → workspace-scoped hooks + org-wide hooks get it; envelope includes "workspace_id"
);
CRM side:
- Expose
https://crm.example.com/integrations/tenantx/inventory. - Verify
X-TenantX-Signature(see OUTGOING_WEBHOOKS.md). - Read
event,organization_id, optionalworkspace_id, anddata. - Upsert a “low stock” or “reorder” task, or call your ERP API.
“Updates the webhooks”: the integration usually does not change Tenantx webhook rows on each receipt. Instead, you register the URL once; each receipt triggers a new delivery with a new id. If you truly need dynamic URLs, use the outgoing webhooks API from your automation (requires a token with outgoing_webhooks.*).
4. Hospital / patient-style example (workspace = site or department)
Map workspace to something operational: hospital campus, clinic, or department (each is a workspace_branding row in the legacy schema).
Tenantx does not include patient PHI or clinical events in the kernel. In your vertical you would:
- Store patients or visits with
organization_idandworkspace_id. - When an event occurs (admission, discharge, lab result ready — your rules), dispatch:
app(OutgoingWebhookDispatcher::class)->dispatch(
$organizationId,
'patient.admitted', // your constant; register in OutgoingWebhookEvent + seeder/validation if exposed in API
[
'patient_reference' => $opaqueId, // never put raw PHI if avoidable; use internal UUIDs
'admitted_at' => now()->toIso8601String(),
],
$workspaceId
);
Receiving system (RIS, CRM, care-coordination tool):
- Org-wide webhook: receives all workspace-tagged events for the org (filter in code by
workspace_idin the JSON). - Workspace-specific webhook: only that campus/department’s events.
Compliance: webhooks cross the network to your endpoint — use TLS, signature verification, minimize payload fields, and follow HIPAA/GDPR policies for your jurisdiction. Tenantx only provides transport; you own payload design and downstream handling.
5. Extending the kernel safely
- Add an event name in
App\Support\OutgoingWebhookEvent(and::all()). - Relax or extend validation in
OrganizationOutgoingWebhookControllersoevent_types.*allows your new string (today it usesRule::in(array_merge(OutgoingWebhookEvent::all(), ['*']))— adding to::all()is enough). - Call the dispatcher from the right place in your code path, with
nullor$workspaceIdas the fourth argument. - Optional: add UI labels in the React webhooks page
EVENT_VALUES/ translations if you want tenants to subscribe selectively to the new event.
6. Stripe and subscription events
subscription.updated is fired from Stripe sync code paths. It is organization-level (no workspace_id in the envelope today). A CRM that only cares about billing can use a single org-wide URL and ignore workspace_id until you add workspace-aware subscription logic.
7. Checklist for integrators
- HTTPS endpoint with valid certificate.
- Verify
X-TenantX-Signatureusing the exact raw body string. - Respond 2xx quickly; offload heavy work to your own queue.
- Dedupe using
id/X-TenantX-Delivery. - Run Tenantx queue workers so deliveries actually leave the app.
- Use organization-wide hooks for org-only kernel events; use workspace hooks when you dispatch with a workspace id.
8. Fresh databases during development
If you collapsed migrations and workspace_id now lives in the original create migration, existing dev DBs that already ran the old table definition may need migrate:fresh (or a manual column add) once. See the note at the bottom of OUTGOING_WEBHOOKS.md.