Sending Events
This page covers everything you need to trigger notifications from your application — the endpoint, request format, channel routing, code examples, and idempotency.
Prerequisites
Before triggering events, make sure you have:
- A tenant API key — provided by your platform operator. Never expose this in client-side code.
- At least one active provider — configured in Admin UI → Providers.
- At least one active template — matching the event type you plan to trigger.
If any of these are missing, events are accepted but nothing is delivered.
Step 1 — Get your credentials
| Credential | Purpose | Where to use |
|---|---|---|
| Tenant API Key | Authenticates every event trigger | x-api-key header |
| Webhook Secret (optional) | HMAC-SHA256 payload signing | Compute and send as X-Orqestra-Signature |
Your Tenant API Key is a privileged secret. Never expose it in client-side code, public repositories, or browser network requests.
Step 2 — Configure a delivery provider in the Admin UI
Go to Admin UI → Providers and add credentials for the channels you intend to use:
| Channel | Supported Providers | What you need |
|---|---|---|
| Resend, SendGrid | API key from the provider dashboard | |
| SMS | Twilio, Africa's Talking | Account SID + Auth Token (Twilio) or API key |
| Real-time Push | Built-in | No external account needed |
Step 3 — Create a template
Go to Admin UI → Templates and create a template:
- Set the event type you will trigger (e.g.
order.confirmed) - Choose the channel (Email, SMS, or Push)
- Write the content using Handlebars variables — any field in your
variablesobject is available as{{fieldName}} - For Email, use the MJML editor for responsive HTML
- Activate the template — inactive templates are ignored at dispatch time
:::tip Global templates
Use one of the built-in global.* event types (global.info, global.success, global.warning, global.alert, global.error) to test without creating custom templates. These resolve against platform-wide templates automatically.
:::
The endpoint
POST /api/v1/events/trigger
Headers
| Header | Required | Description |
|---|---|---|
Content-Type | Yes | Must be application/json |
x-api-key | Yes | Your tenant API key |
X-Orqestra-Signature | Conditional | Required if your tenant has a webhook secret configured. See Webhook Security. |
X-Idempotency-Key | No | Custom deduplication key. See Idempotency. |
X-Trace-Id | No | Trace ID for distributed tracing and debugging |
Request body
{
"eventType": "order.confirmed",
"payload": {
"userId": "uuid-of-the-recipient",
"recipientEmail": "customer@example.com",
"recipientPhone": "+254712345678"
},
"variables": {
"orderNumber": "ORD-2026-1234",
"amount": "$49.99",
"customerName": "Alice"
}
}
payload carries routing identity — who to notify and on which address. variables carries template data — the values that get interpolated into your message content at render time.
| Field | Type | Required | Description |
|---|---|---|---|
eventType | string | Yes | The event identifier matching a configured template |
eventId | string | No | Explicit event ID (auto-generated if omitted) |
channels | string[] | No | Per-trigger channel override. Restricts this dispatch to listed channels only. Valid: "EMAIL", "SMS", "PUSH". |
payload.userId | string | Yes | The recipient's user ID in your system |
payload.recipientEmail | string | No | Required for email channel |
payload.recipientPhone | string | No | Required for SMS channel (E.164 format) |
variables | object | No | Arbitrary key/value pairs available as {{fieldName}} in your templates. Shape depends entirely on what your template expects — there are no reserved keys. |
Successful response
{
"success": true,
"message": "Event order.confirmed dispatched securely for YourTenantName"
}
Channel routing
By default, every active template matching the eventType is dispatched (Email, SMS, Push simultaneously). Two controls narrow this:
Tenant channel policy (set by platform operator)
Each tenant has a delivery_channels whitelist:
| Value | Behaviour |
|---|---|
[] empty | All channels with matching active templates dispatch |
["EMAIL"] | Only email templates dispatch |
["EMAIL", "PUSH"] | Email and push dispatch; SMS skipped |
Per-trigger override (set by your application)
{
"eventType": "global.alert",
"channels": ["EMAIL"],
"payload": { "userId": "...", "recipientEmail": "..." },
"variables": { "title": "...", "message": "..." }
}
The per-trigger channels filter applies after the tenant policy. It can narrow further but cannot expand beyond what the tenant policy permits.
Global templates
Five built-in event types work without custom templates:
eventType | Use case |
|---|---|
global.info | Informational updates |
global.success | Positive outcomes, approvals |
global.warning | Reminders, upcoming deadlines |
global.alert | Important issues, urgent state |
global.error | System or workflow failures |
Recommended shape for global events:
{
"eventType": "global.success",
"payload": {
"userId": "uuid",
"recipientEmail": "user@example.com"
},
"variables": {
"title": "Application Approved",
"message": "Your loan application has been approved."
}
}
Always include title and message in variables — these are the minimum fields expected by the built-in global templates.
Code examples
cURL
curl -X POST https://your-orqestra-host/api/v1/events/trigger \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_TENANT_API_KEY" \
-d '{
"eventType": "order.confirmed",
"payload": {
"userId": "b5dae3e8-2d5d-6f3c-cd3c-3c4d5e6f7a8b",
"recipientEmail": "customer@example.com"
},
"variables": {
"orderNumber": "ORD-2026-1234",
"amount": "$49.99",
"customerName": "Alice"
}
}'
Node.js / TypeScript
const response = await fetch('https://your-orqestra-host/api/v1/events/trigger', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.ORQESTRA_API_KEY!,
},
body: JSON.stringify({
eventType: 'order.confirmed',
payload: {
userId: user.id,
recipientEmail: user.email,
},
variables: {
orderNumber: order.number,
amount: order.formattedTotal,
customerName: user.firstName,
},
}),
});
const result = await response.json();
Python
import requests, os
response = requests.post(
"https://your-orqestra-host/api/v1/events/trigger",
headers={
"Content-Type": "application/json",
"x-api-key": os.environ["ORQESTRA_API_KEY"],
},
json={
"eventType": "order.confirmed",
"payload": {
"userId": user_id,
"recipientEmail": user_email,
},
"variables": {
"orderNumber": "ORD-2026-1234",
"amount": "$49.99",
"customerName": customer_name,
},
},
)
Go
body, _ := json.Marshal(map[string]any{
"eventType": "order.confirmed",
"payload": map[string]any{
"userId": userID,
"recipientEmail": userEmail,
},
"variables": map[string]any{
"orderNumber": "ORD-2026-1234",
"amount": "$49.99",
"customerName": customerName,
},
})
req, _ := http.NewRequest("POST", orqestraURL+"/api/v1/events/trigger", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", os.Getenv("ORQESTRA_API_KEY"))
resp, err := http.DefaultClient.Do(req)
Idempotency
The engine automatically deduplicates events using a composite key of tenantId + eventType + payload. If your backend retries a failed request, the same event won't be processed twice.
To override deduplication (e.g. for intentional reminder resends), provide a unique key:
curl -X POST https://your-orqestra-host/api/v1/events/trigger \
-H "x-api-key: YOUR_KEY" \
-H "X-Idempotency-Key: reminder-2026-04-22-user-001" \
-H "Content-Type: application/json" \
-d '{ "eventType": "global.warning", "payload": { "userId": "...", "recipientEmail": "..." }, "variables": { "title": "...", "message": "..." } }'
Idempotency entries expire after 24 hours.
Rate limits
| Limit Type | Default | Behaviour |
|---|---|---|
| Per-minute | Configured per tenant | Sliding window counter |
| Daily cap | Configured per tenant | Resets at midnight UTC |
When exceeded:
{
"statusCode": 429,
"error": "Too Many Requests",
"message": "Rate limit exceeded for tenant YourApp. Limit: 100/min",
"retryAfter": 45
}
The response includes a Retry-After header with seconds to wait.
Error responses
| Status | Meaning | Common cause |
|---|---|---|
200 | Accepted | Success |
401 | Authentication failed | Invalid/missing x-api-key, invalid HMAC signature, inactive tenant |
422 | Validation error | Missing eventType, payload, or payload.userId |
429 | Rate limit exceeded | Too many requests — respect Retry-After |
500 | Internal error | Kafka unavailable, database error |
Next steps
- Webhook Security → — Sign your payloads with HMAC-SHA256.
- Templates Guide → — Write MJML email and Handlebars templates.
- Real-time Setup → — Add a WebSocket notification feed to your frontend.