Skip to main content

Outbound webhook events

CRM publishes outbound integration events onto the RabbitMQ exchange under the webhook.{event_type} routing key. The downstream tenant-backend-webhook service fans these out to per-tenant HTTP subscribers.

This is distinct from the inbound identity events (where CRM is a consumer) — see events.md for that direction. It is also distinct from the WebSocket fan-out done via the ws-proxy (also documented below) which targets the SPA, not external subscribers.

Transport

ExchangeRabbitMQ default exchange via RabbitMQService::publish
Routing key shapewebhook.{event_type}
Publisher classApp\Services\WebhookEventPublisher
Sync methodpublish(string $eventType, array $data): void
Async methodpublishAsync(...) — defers to app()->terminating() so the HTTP response ships first
Envelope{ event, data, timestamp, service: 'crm' }

A failure to publish is logged at error level and the request still succeeds — webhook delivery is best-effort. Subscribers must tolerate missing events; ordering is not guaranteed.

Event catalogue

The list below is generated from grep publishAsync over app/Http/Controllers/. When you add a webhook publisher call, add the event name here in the same commit.

Chat

EventSource controllerPayload
chat.channel.createdChatControllerChatChannel model array
chat.message.sentChatMessageController, ChatFileControllerChatMessage model array (file or text)
chat.message.updatedChatMessageControllerChatMessage model array
chat.message.deletedChatMessageController{ id, channel_id }
chat.message.recalledChatMessageController{ id, channel_id, recalled_at }
chat.participant.addedChatParticipantController{ channel_id, user_id, added_by }
chat.participant.removedChatParticipantController{ channel_id, user_id, removed_by }

Content (Blog / Page / KB)

EventSource controllerPayload
blog_post.createdBlogPostControllerBlogPost model array
blog_post.updatedBlogPostControllerBlogPost model array
blog_post.deletedBlogPostController{ id }
blog_category.createdBlogCategoryControllerBlogCategory model array
blog_category.updatedBlogCategoryControllerBlogCategory model array
blog_category.deletedBlogCategoryController{ id }
page.createdPageControllerPage model array
page.updatedPageControllerPage model array
page.deletedPageController{ id }
page_category.createdPageCategoryControllerPageCategory model array
page_category.updatedPageCategoryControllerPageCategory model array
page_category.deletedPageCategoryController{ id }

Publish/unpublish of blog_post and page is currently emitted as *.updated (the published_at field flips). The publish lifecycle is mirrored in ActivityLog rows for the compliance trail — see security.md § audit trail.

SMS templates

EventSource controllerPayload
sms_template.createdSmsTemplateControllerSmsTemplate model array
sms_template.updatedSmsTemplateControllerSmsTemplate model array
sms_template.deletedSmsTemplateController{ id }

CRM core

EventSource controllerPayload
crm.company.types_changedCompanyController{ company_id, previous, current }

Envelope shape

{
"event": "blog_post.updated",
"data": { "id": "01J6...", "title": "...", "...": "..." },
"timestamp": 1715600000,
"service": "crm"
}

Subscriber-side deduplication should key on (event, data.id, timestamp) because the publisher does not mint per-event UUIDs.

WebSocket fan-out (ws-proxy, distinct from HTTP webhooks)

These are emitted by App\Services\EventPublisher onto the ws-proxy.{channel}.{event} (or ws-proxy.user.{userId}.{event}) routing key. The ws-proxy service forwards them to subscribed browser clients via the websocket connection — they are not external webhook deliveries.

EventTarget
chat.message.newper-participant user channel
chat.message.updatedper-participant user channel
chat.message.deletedper-participant user channel
chat.message.recalledper-participant user channel
chat.reaction.updatedper-participant user channel

The SPA's useChatStore subscribes to the user channel on login and applies these patches locally.

Adding a new webhook event

  1. Pick a name that follows {resource}.{verb} (task.created not created_task).
  2. Inject WebhookEventPublisher (or pull from $this->webhookPublisher if the parent controller has it).
  3. Call publishAsync($name, $payload) after the DB commit — the terminating() defer means a publish failure can't 500 the response.
  4. Add a row to the relevant table above in the same PR.
  5. If the payload includes sensitive fields, scrub them at the publish site (the downstream webhook service has no field-level filtering).

Where to learn more

  • events.md — inbound identity → CRM events
  • api.md — REST endpoints that trigger these emits
  • tenant/tenant-backend-crm/app/Services/WebhookEventPublisher.php — the publisher
  • tenant/tenant-backend-crm/app/Services/EventPublisher.php — the ws-proxy publisher
  • documentations/docs/tenant-services/webhook.md — the downstream subscriber service