Documentation

Webhooks and events

How AppEngine emits events and how to receive them in your own systems.

AppEngine has no first-class "webhook module" — there is no POST /webhook/register endpoint that delivers a JSON payload to your URL on every domain event. Instead, events flow through three layers internally, and integrations subscribe through a fourth layer (the Automation module). This page maps the model so you know where to plug in.

The four layers

  1. NestJS EventEmitter — synchronous, in-process. Domain controllers emit events (contact.created, order.paid, lead.qualified); other in-process services listen.
  2. Queue consumers (BullMQ / Redis) — durable, retryable. Long-running side effects (notifications, social sync, escalation, billing) are pushed onto Redis-backed queues and processed by *Consumer services.
  3. Per-vendor webhook receivers — inbound. AppEngine itself accepts webhooks from Stripe, PayPal, SendGrid, Mailgun, Twilio, social platforms, etc., and translates them into internal events.
  4. Automation actions — outbound. The AutomationModule exposes a "send webhook" action you can wire to any internal event. This is the integrator-facing way to forward events to your own systems.

If you want to "receive webhooks from AppEngine when a customer signs up," you do it via layer 4: define an automation, set the trigger to contact.created, set the action to send webhook with your URL.

In-process events

Inside the NestJS app, controllers emit events using EventEmitter2:

// pseudocode — pulled from various controllers
this.eventEmitter.emit('contact.created', { orgId, contact });
this.eventEmitter.emit('order.paid', { orgId, orderId, amount });
this.eventEmitter.emit('lead.scored', { orgId, leadId, score });

Listeners subscribe with @OnEvent('contact.created') decorators. These run synchronously in the same process — fast but tied to the request lifecycle. If a listener throws, the originating request can fail.

This layer is internal. You can't subscribe from outside the AppEngine process. It's documented here so you understand why automations and queue consumers exist on top of it.

Queue consumers

Long-running or unreliable work moves to BullMQ queues:

QueuePurpose
notificationSend email/SMS/push, retry on failure
automationExecute automation flows
automation-triggerWatch DB changes and fire automation triggers
scheduleCron-driven scheduled jobs
social-syncFB/TikTok/LinkedIn/Twitter/Pinterest/Snapchat/Discord/Twitch/Slack/Reddit ingestion
escalationSLA-based ticket escalation
billingPeriodic billing/invoice generation

Queues retry on failure with exponential backoff, persist to Redis across process restarts, and surface in the monitoring dashboard. You don't interact with queues directly — you trigger them by calling domain endpoints.

Inbound vendor webhooks

AppEngine receives webhooks from third-party providers and turns them into internal events. The receivers live in their own controllers and verify signatures per vendor:

VendorReceiver pathTriggers
Stripe/storefront/stripe/webhook (inferred — verify per deployment)payment.succeeded, subscription.renewed, etc.
PayPal/storefront/paypal/webhookorder completed, refund
SendGrid / Mailgun / SES/broadcast/email/webhookdelivery, open, click, bounce
Twilio/phone/webhook/*inbound call, SMS received, status update
Social platforms/sync/social/webhook/{vendor}new post, mention, comment

When you connect a vendor through the OAuth flow (/upstream/connect/{vendor} or /connect/{vendor}/auth-url), AppEngine registers the right webhook URL with that vendor automatically. You don't configure the URL by hand.

These are inbound-only — they let AppEngine react to vendor events. They are NOT an outbound delivery mechanism for your integration to pick up.

Outbound webhooks via Automation

This is the layer integrators use. The AutomationModule provides a flow builder where:

  • Triggers include "scheduled", "contact created", "email opened", "appointment booked", "page visited", "tagged", "DB change".
  • Actions include "create/update/delete data", "send notification", "make call", "create task", "add tag", and "send webhook".

To get an outbound webhook to your URL on every new contact:

  1. 1

    Create the automation

    curl -X POST https://appengine.appmint.io/automation/flows/create \
      -H "orgid: my-org" -H "Authorization: Bearer $JWT" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Notify CRM bridge",
        "trigger": { "type": "contact.created" },
        "actions": [
          {
            "type": "send_webhook",
            "config": {
              "url": "https://my-bridge.example.com/appmint/contact",
              "method": "POST",
              "headers": { "X-Bridge-Secret": "shared-secret-here" }
            }
          }
        ]
      }'
    
  2. 2

    Verify with a test event

    curl -X POST https://appengine.appmint.io/automation/flows/{flowId}/execute \
      -H "orgid: my-org" -H "Authorization: Bearer $JWT" \
      -H "Content-Type: application/json" \
      -d '{ "context": { "contact": { "email": "[email protected]" } } }'
    
  3. 3

    Receive on your side

    Your endpoint gets a POST with a JSON body shaped roughly like:

    {
      "event": "contact.created",
      "orgId": "my-org",
      "timestamp": "2026-04-25T10:00:00Z",
      "data": { "sk": "contact-abc", "datatype": "contact", "data": { "email": "[email protected]" } }
    }
    

    Verify the X-Bridge-Secret header (or whatever auth you configured) before processing.

The full set of triggers and conditions is documented in the Automation module reference.

Polling fallback

If an automation isn't a fit (e.g., you want to backfill historical data), poll a list endpoint with a modifydate filter:

POST /repository/find/contact
{
  "query": { "modifydate": { "$gte": "2026-04-25T00:00:00Z" } },
  "options": { "sort": { "modifydate": -1 }, "pageSize": 100 }
}

Track the highest modifydate you've seen and use it as the next watermark. Combined with idempotent processing on your side, this is reliable and easy to reason about.

SSE for AI streams

AI agent responses use Server-Sent Events, not webhooks. Two-step pattern:

# 1. start a stream — returns { streamId }
curl -X POST https://appengine.appmint.io/ai/agent/stream \
  -H "orgid: my-org" -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{ "agentId": "...", "message": "Hello" }'

# 2. consume the stream
curl -N https://appengine.appmint.io/ai/stream/{streamId} \
  -H "orgid: my-org" -H "Authorization: Bearer $JWT"

-N disables curl's buffering so you see tokens as they arrive. SSE messages are data: { ... }\n\n-framed.

WebSockets for chat and voice

Real-time chat and voice ride on Socket.IO gateways:

  • ChatGateway at /chat — staff/customer messaging, presence, queue routing.
  • CommunityChatGateway at /community-chat — group chats.
  • SimpleVoiceGateway and AIVoiceGateway at /voice — OpenAI Realtime audio streaming.

Connect with the standard Socket.IO client, send the orgid and JWT during the handshake. Per-gateway events are documented in the Chat and Voice module references.

What this means for you

If you're integrating from outside AppEngine:

  1. For "tell me when X happens" — set up an automation with a send webhook action.
  2. For "let me ingest historical data" — poll a list endpoint with modifydate >= watermark.
  3. For "stream AI responses" — use the SSE pattern above.
  4. For "real-time chat" — connect to the Socket.IO gateway.

There is no other event delivery mechanism today. If your use case doesn't fit, file a feature request — first-class outbound webhooks (independent of automation) are on the roadmap.