Messages
Apps can send push notifications and emails to users through a durable message queue. Messages are enqueued via the notify() and email() callbacks in server route handlers, webhook handlers, and agent tools, then delivered asynchronously by a background dispatcher with retry and dead-letter handling.
Message Lifecycle
pending → dispatched → delivered (success)
pending → dispatched → pending (failure, will retry)
pending → dispatched → dead (max retries exceeded)
dead/failed → pending (manual retry via API)
Channels
| Channel | Recipient | Delivery Method |
|---|---|---|
push | Informer username | FCM to all registered devices (Informer GO) |
email | Email address | Tenant mail transport (SMTP, Gmail API, Microsoft Graph) |
Push notifications include the app's ID automatically — tapping a notification in Informer GO opens the app, optionally at a specific sub-page via the path field.
GET /api/apps/{id}/messages
List messages for an app with optional filters and cursor-based pagination.
Authentication: Required
Permissions Required: Member+ role (write permission)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
channel | string | — | Filter by channel: push or email |
status | string | — | Filter by status: pending, dispatched, delivered, failed, dead |
recipient | string | — | Filter by recipient (exact match) |
limit | number | 100 | Max results (1–500) |
before | ISO date | — | Cursor: return messages created before this timestamp |
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/messages"
}
},
"messages": [
{
"id": "00000000-0000-4000-8000-000000000001",
"channel": "push",
"recipient": "annie.larson",
"payload": {
"body": "Order #1042 left the warehouse",
"path": "/orders/1042",
"title": "Order shipped"
},
"status": "delivered",
"attempts": 1,
"maxAttempts": 3,
"error": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"processedAt": "2026-01-15T16:20:00.000Z"
},
{
"id": "00000000-0000-4000-8000-000000000002",
"channel": "push",
"recipient": "abel.jacobs",
"payload": {
"body": "Chai is below reorder point",
"title": "Low stock"
},
"status": "pending",
"attempts": 0,
"maxAttempts": 3,
"error": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"processedAt": null
},
{
"id": "00000000-0000-4000-8000-000000000003",
"channel": "email",
"recipient": "carol@example.com",
"payload": {
"to": "carol@example.com",
"html": "<p>12 orders today</p>",
"subject": "Daily order summary"
},
"status": "delivered",
"attempts": 1,
"maxAttempts": 3,
"error": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"processedAt": "2026-01-15T16:20:00.000Z"
},
"… 1 more items (4 total) — omitted from docs"
],
"summary": [
{
"status": "delivered",
"count": 2
},
{
"status": "failed",
"count": 1
},
{
"status": "pending",
"count": 1
}
],
"daily": [
{
"day": "2026-01-15",
"channel": "email",
"count": 2
},
{
"day": "2026-01-15",
"channel": "push",
"count": 2
}
]
}
The summary provides status counts (useful for dashboard indicators) and daily provides per-day, per-channel volume.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/messages"
}
},
"messages": [
{
"id": "00000000-0000-4000-8000-000000000001",
"channel": "email",
"recipient": "carol@example.com",
"payload": {
"to": "carol@example.com",
"html": "<p>12 orders today</p>",
"subject": "Daily order summary"
},
"status": "delivered",
"attempts": 1,
"maxAttempts": 3,
"error": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"processedAt": "2026-01-15T16:20:00.000Z"
},
{
"id": "00000000-0000-4000-8000-000000000002",
"channel": "email",
"recipient": "dave@example.com",
"payload": {
"to": "dave@example.com",
"html": "<p>1 failure</p>",
"subject": "Webhook failure digest"
},
"status": "failed",
"attempts": 2,
"maxAttempts": 3,
"error": "Connection refused",
"createdAt": "2026-01-15T16:20:00.000Z",
"processedAt": null
}
],
"summary": [
{
"status": "delivered",
"count": 2
},
{
"status": "failed",
"count": 1
},
{
"status": "pending",
"count": 1
}
],
"daily": [
{
"day": "2026-01-15",
"channel": "email",
"count": 2
},
{
"day": "2026-01-15",
"channel": "push",
"count": 2
}
]
}
Cursor Pagination:
Use the createdAt of the last message as the before parameter:
// First page
const page1 = await fetch('/api/apps/{id}/messages?limit=50');
// Next page
const lastDate = page1.messages[page1.messages.length - 1].createdAt;
const page2 = await fetch(`/api/apps/{id}/messages?limit=50&before=${lastDate}`);
GET /api/apps/{id}/messages/{messageId}
Get a single message by ID.
Authentication: Required
Permissions Required: Member+ role (write permission)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
messageId | string | Message UUID |
Response: Single message object (same shape as items in the list response).
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/messages/00000000-0000-4000-8000-000000000001"
}
},
"id": "00000000-0000-4000-8000-000000000001",
"tenant": "acme",
"appId": "00000000-0000-4000-8000-000000000002",
"channel": "push",
"recipient": "annie.larson",
"payload": {
"body": "Order #1042 left the warehouse",
"path": "/orders/1042",
"title": "Order shipped"
},
"status": "delivered",
"attempts": 1,
"maxAttempts": 3,
"error": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"processedAt": "2026-01-15T16:20:00.000Z"
}
Error Responses:
404 Not Found— Message does not exist or does not belong to this app
POST /api/apps/{id}/messages/{messageId}/_retry
Retry a failed or dead message. Resets the message to pending status with zero attempts so the dispatcher will pick it up again.
Authentication: Required
Permissions Required: Member+ role (write permission)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
messageId | string | Message UUID |
Response: The updated message object with status: "pending".
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/messages/00000000-0000-4000-8000-000000000001/_retry"
}
},
"id": "00000000-0000-4000-8000-000000000001",
"tenant": "acme",
"appId": "00000000-0000-4000-8000-000000000002",
"channel": "email",
"recipient": "dave@example.com",
"payload": {
"to": "dave@example.com",
"html": "<p>1 failure</p>",
"subject": "Webhook failure digest"
},
"status": "pending",
"attempts": 0,
"maxAttempts": 3,
"error": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"processedAt": null
}
Error Responses:
404 Not Found— Message does not exist409 Conflict— Message is not in a retryable state (e.g., alreadypendingordelivered)
Sending Messages from App Code
Messages are sent from server route handlers, webhook handlers, and agent tools using the notify() and email() callbacks. See the handler documentation for full API details.
Quick Reference
// Push notification
const { id } = await notify('username', {
title: 'Alert',
body: 'Something happened',
path: '/details/123' // optional deep link
});
// Email
const { id } = await email('user@example.com', {
subject: 'Report Ready',
html: '<p>Your report is attached.</p>',
from: 'noreply@example.com' // optional
});
// Bulk push
const { ids, queued } = await notify([
{ username: 'alice', title: 'Update', body: '...' },
{ username: 'bob', title: 'Update', body: '...' },
]);
// Bulk email
const { ids, queued } = await email([
{ to: 'alice@example.com', subject: 'Update', html: '...' },
{ to: 'bob@example.com', subject: 'Update', html: '...' },
]);
HAL Relations
| Relation | Description |
|---|---|
inf:app-messages | Link to the app's message list |
inf:app-message | Link to a single message |
inf:app-message-retry | Retry a failed/dead message |