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
- NestJS EventEmitter — synchronous, in-process. Domain controllers emit events (
contact.created,order.paid,lead.qualified); other in-process services listen. - Queue consumers (BullMQ / Redis) — durable, retryable. Long-running side effects (notifications, social sync, escalation, billing) are pushed onto Redis-backed queues and processed by
*Consumerservices. - Per-vendor webhook receivers — inbound. AppEngine itself accepts webhooks from Stripe, PayPal, SendGrid, Mailgun, Twilio, social platforms, etc., and translates them into internal events.
- Automation actions — outbound. The
AutomationModuleexposes 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:
| Queue | Purpose |
|---|---|
notification | Send email/SMS/push, retry on failure |
automation | Execute automation flows |
automation-trigger | Watch DB changes and fire automation triggers |
schedule | Cron-driven scheduled jobs |
social-sync | FB/TikTok/LinkedIn/Twitter/Pinterest/Snapchat/Discord/Twitch/Slack/Reddit ingestion |
escalation | SLA-based ticket escalation |
billing | Periodic 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:
| Vendor | Receiver path | Triggers |
|---|---|---|
| Stripe | /storefront/stripe/webhook (inferred — verify per deployment) | payment.succeeded, subscription.renewed, etc. |
| PayPal | /storefront/paypal/webhook | order completed, refund |
| SendGrid / Mailgun / SES | /broadcast/email/webhook | delivery, 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
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
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
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-Secretheader (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:
ChatGatewayat/chat— staff/customer messaging, presence, queue routing.CommunityChatGatewayat/community-chat— group chats.SimpleVoiceGatewayandAIVoiceGatewayat/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:
- For "tell me when X happens" — set up an automation with a
send webhookaction. - For "let me ingest historical data" — poll a list endpoint with
modifydate >= watermark. - For "stream AI responses" — use the SSE pattern above.
- 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.