Docs
Outgoing webhooks — how to integrate and extend

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.

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, and pro: not available
  • enterprise: 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:

Scopeworkspace_id in APIWho receives deliveries
Entire organizationnullHooks 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 workspaceUUID of workspace_brandingOnly 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:

  1. Domain tables (e.g. inventory_items with organization_id, workspace_id).
  2. 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:

  1. Expose https://crm.example.com/integrations/tenantx/inventory.
  2. Verify X-TenantX-Signature (see OUTGOING_WEBHOOKS.md).
  3. Read event, organization_id, optional workspace_id, and data.
  4. 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:

  1. Store patients or visits with organization_id and workspace_id.
  2. 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_id in 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

  1. Add an event name in App\Support\OutgoingWebhookEvent (and ::all()).
  2. Relax or extend validation in OrganizationOutgoingWebhookController so event_types.* allows your new string (today it uses Rule::in(array_merge(OutgoingWebhookEvent::all(), ['*'])) — adding to ::all() is enough).
  3. Call the dispatcher from the right place in your code path, with null or $workspaceId as the fourth argument.
  4. 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-Signature using 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.