Parent Document: API_Reference.md Implementation Folder: app/src/Task/ Codeunit: Cloud Event Message Events (Codeunit 65346)
The Cloud Events extension provides native Business Central External Business Events that enable external systems to receive webhook notifications when cloud event messages complete or fail. This allows for event-driven architectures where external systems are notified immediately rather than polling for status.
Key Features:
Business Central's External Business Events allow external systems to subscribe to events and receive HTTP POST notifications when events occur. The Cloud Events extension exposes two external business events:
All Cloud Events webhook notifications are categorized under:
Cloud Event CategoryThis category can be used to filter and organize event subscriptions in Business Central.
Webhook notifications intentionally send minimal data to:
After receiving a webhook notification, subscribers call the Cloud Event Data API using the MessageId to retrieve the full response.
Purpose: Notifies external systems when a cloud event message has completed processing successfully.
Event Name: CloudEventMessageCompleted Event Display Name: Cloud Event Message Completed Event Category: Origo Cloud Event Raised By: Codeunit 65313 Cloud Event Message Task
The event is raised after a cloud event message has been successfully processed:
Cloud Event Message table{
"MessageId": "a8f5f167-8f2c-4a42-9b3e-5c6c7d8e9f0a",
"MessageType": "Customer.CreditLimit.Get",
"ResponseContentLink": "/api/origo/cloudEvent/v1.0/responses(a8f5f167-8f2c-4a42-9b3e-5c6c7d8e9f0a)/data",
"Timestamp": "2026-03-08T14:30:22Z"
}
| Field | Type | Description |
|---|---|---|
MessageId | Guid | Unique identifier for the message. Use this to call GET /cloudEventData(MessageId) to retrieve full response. |
MessageType | Text[250] | The type of message that completed (e.g., "Customer.CreditLimit.Get", "Data.Records.Get"). Can be used for routing or filtering. |
ResponseContentLink | Text[250] | Direct API link to download response data. Use this URL to retrieve the full response without constructing the API path manually. |
Timestamp | DateTime | When the message completed processing (ISO 8601 format). |
After receiving the webhook notification, call the Cloud Event Data API to retrieve the full response:
Request:
GET /api/origo/cloudEvent/v1.0/responses('{message-id}')
Authorization: Bearer {token}
Response:
{
"id": "a8f5f167-8f2c-4a42-9b3e-5c6c7d8e9f0a",
"data": "... full response data as base64 or JSON ..."
}
sequenceDiagram
participant External as External System
participant BCQueue as BC Queue API
participant BCTask as BC Task Processor
participant Webhook as Webhook Endpoint
participant BCData as BC Data API
External->>BCQueue: POST /cloudEventQueue (message)
BCQueue-->>External: 202 Accepted (MessageId)
BCTask->>BCTask: Process message
BCTask->>BCTask: Store response data
BCTask->>Webhook: POST webhook (MessageId, Type, Time)
Webhook->>BCData: GET /cloudEventData(MessageId)
BCData-->>Webhook: Response data
Webhook->>Webhook: Process response
Purpose: Notifies external systems when a cloud event message processing has failed.
Event Name: CloudEventMessageFailed Event Display Name: Cloud Event Message Failed Event Category: Origo Cloud Event Raised By: Codeunit 65312 Cloud Event Message Error
The event is raised after a cloud event message processing has failed:
Cloud Event Message table{
"MessageId": "b9f6f267-9f3d-5b52-0c4f-6d7d8e9f1b1b",
"MessageType": "Data.Records.Set",
"ResponseContentLink": "/api/origo/cloudEvent/v1.0/responses(b9f6f267-9f3d-5b52-0c4f-6d7d8e9f1b1b)/data",
"Timestamp": "2026-03-08T14:35:18Z"
}
| Field | Type | Description |
|---|---|---|
MessageId | Guid | Unique identifier for the message. Use this to call GET /cloudEventQueue(MessageId) to retrieve error details. |
MessageType | Text[250] | The type of message that failed (e.g., "Data.Records.Set", "Sales.Document.Release"). |
ResponseContentLink | Text[250] | Direct API link to download error details. Use this URL to retrieve the error response without constructing the API path manually. |
Timestamp | DateTime | When the message failed processing (ISO 8601 format). |
After receiving the webhook notification, call the Cloud Event Queue API to retrieve error details:
Request:
GET /api/origo/cloudEvent/v1.0/queues('{message-id}')
Authorization: Bearer {token}
Response:
{
"id": "b9f6f267-9f3d-5b52-0c4f-6d7d8e9f1b1b",
"type": "Data.Records.Set",
"specversion": "1.0",
"source": "MyIntegrationApp v1.0",
"time": "2026-03-08T14:35:15Z",
"datacontenttype": "text/json",
"data": "{
\"error\": \"Record not found\",
\"detailedMessage\": \"Table: Customer, SystemId: {guid}\",
\"stackTrace\": \"...\",
\"callStack\": \"...\"
}"
}
Error responses are stored in JSON format with the following fields:
sequenceDiagram
participant External as External System
participant BCQueue as BC Queue API
participant BCTask as BC Task Processor
participant Webhook as Webhook Endpoint
participant BCQueue2 as BC Queue API
External->>BCQueue: POST /cloudEventQueue (message)
BCQueue-->>External: 202 Accepted (MessageId)
BCTask->>BCTask: Process message
BCTask->>BCTask: ERROR OCCURS
BCTask->>BCTask: Store error data
BCTask->>Webhook: POST webhook (MessageId, Type, Time)
Webhook->>BCQueue2: GET /cloudEventQueue(MessageId)
BCQueue2-->>Webhook: Error details
Webhook->>Webhook: Log error & alert
In addition to External Business Events for webhooks, the Cloud Events extension provides Integration Events that allow other Business Central extensions to react to message lifecycle events.
Purpose: Raised before a cloud event message starts processing.
Event Type: IntegrationEvent Visibility: Internal Raised By: Codeunit 65313 Cloud Event Message Task
Signature:
[IntegrationEvent(false, false)]
internal procedure OnBeforeCloudEventMessageProcessing(var CloudEventMessage: Record "Cloud Event Message")
Parameters:
CloudEventMessage: The message record about to be processed (passed by reference, can be modified)Use Cases:
Purpose: Raised after a cloud event message completes successfully.
Event Type: IntegrationEvent Visibility: Internal Raised By: Codeunit 65313 Cloud Event Message Task
Signature:
[IntegrationEvent(false, false)]
internal procedure OnAfterCloudEventMessageCompleted(var CloudEventMessage: Record "Cloud Event Message")
Parameters:
CloudEventMessage: The completed message record (passed by reference)Use Cases:
Purpose: Raised after a cloud event message processing fails.
Event Type: IntegrationEvent Visibility: Internal Raised By: Codeunit 65312 Cloud Event Message Error
Signature:
[IntegrationEvent(false, false)]
internal procedure OnAfterCloudEventMessageFailed(var CloudEventMessage: Record "Cloud Event Message"; ErrorText: Text)
Parameters:
CloudEventMessage: The failed message record (passed by reference)ErrorText: The error message textUse Cases:
| Field | Value | Description |
|---|---|---|
| Subscriber ID | (Auto-generated) | Unique identifier for the subscription |
| Event Name | CloudEventMessageCompleted or CloudEventMessageFailed | Choose which event to subscribe to |
| Company Name | Your company name | Company context for the event |
| Event Category | Origo Cloud Event | Filter to Cloud Events category |
| Endpoint URL | https://your-domain.com/webhook/bc-cloud-events | Your webhook endpoint URL |
| Authentication | (Select method) | How to authenticate to your endpoint |
Choose authentication method:
Option 1: OAuth 2.0 (Recommended)
Option 2: Basic Authentication
Option 3: API Key
Option 4: None
Your endpoint should handle duplicate notifications gracefully:
// Example: Node.js Express endpoint
app.post('/webhook/bc-cloud-events', async (req, res) => {
const { MessageId, MessageType, ResponseContentLink, Timestamp } = req.body;
// Check if we've already processed this message
const exists = await db.checkMessageProcessed(MessageId);
if (exists) {
console.log(`Duplicate notification for ${MessageId}, ignoring`);
return res.status(200).send('OK'); // Still return 200 to prevent retries
}
// Mark as processed before fetching data
await db.markMessageProcessing(MessageId);
// Fetch full response data from BC using the provided link
const response = await fetchCloudEventData(ResponseContentLink);
// Process the response
await processResponse(response, MessageType);
// Mark as completed
await db.markMessageCompleted(MessageId);
res.status(200).send('OK');
});
Respond to webhook quickly and process data asynchronously:
app.post('/webhook/bc-cloud-events', async (req, res) => {
const { MessageId, MessageType, ResponseContentLink, Timestamp } = req.body;
// Immediately queue for background processing
await queue.enqueue({
messageId: MessageId,
messageType: MessageType,
responseContentLink: ResponseContentLink,
timestamp: Timestamp
});
// Respond immediately
res.status(200).send('OK');
});
// Background worker processes the queue
backgroundWorker.on('job', async (job) => {
const response = await fetchCloudEventData(job.responseContentLink);
await processResponse(response, job.messageType);
});
Implement proper error handling and logging:
app.post('/webhook/bc-cloud-events', async (req, res) => {
try {
const { MessageId, MessageType, ResponseContentLink, Timestamp } = req.body;
// Validate payload
if (!MessageId || !MessageType || !ResponseContentLink || !Timestamp) {
console.error('Invalid payload received', req.body);
return res.status(400).send('Invalid payload');
}
// Queue for processing
await queue.enqueue({
messageId: MessageId,
messageType: MessageType,
responseContentLink: ResponseContentLink,
timestamp: Timestamp
});
res.status(200).send('OK');
} catch (error) {
console.error('Webhook processing error:', error);
// Return 4xx for client errors (don't retry)
// Return 5xx for server errors (BC will retry)
res.status(500).send('Internal Server Error');
}
});
Handle retries with exponential backoff when fetching data from BC:
async function fetchCloudEventData(messageId, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await bcApi.get(`/cloudEventData(${messageId})`);
return response.data;
} catch (error) {
if (attempt === maxRetries) throw error;
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, attempt - 1) * 1000;
await sleep(delay);
}
}
}
Implement monitoring for webhook delivery failures:
app.post('/webhook/bc-cloud-events', async (req, res) => {
const startTime = Date.now();
try {
const { MessageId, MessageType, Timestamp } = req.body;
await queue.enqueue({
messageId: MessageId,
messageType: MessageType,
timestamp: Timestamp
});
// Track success metrics
metrics.webhookReceived(MessageType);
metrics.webhookLatency(Date.now() - startTime);
res.status(200).send('OK');
} catch (error) {
// Track failure metrics
metrics.webhookFailed(error);
// Alert on critical failures
if (shouldAlert(error)) {
alerting.sendAlert('Webhook processing failure', error);
}
res.status(500).send('Internal Server Error');
}
});
Submit a test message to trigger webhook notifications:
# Submit test message to Queue API
curl -X POST "https://your-bc-instance/api/origo/cloudEvent/v1.0/queues" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"specversion": "1.0",
"type": "Help.Tables.Get",
"source": "Webhook Test v1.0"
}'
CloudEventMessageCompleted or CloudEventMessageFailed)Business Central may send duplicate notifications in certain scenarios:
Solution: Implement idempotency in your webhook endpoint (see Best Practices above)
Always use authentication for webhook endpoints:
Business Central enforces HTTPS for webhook endpoints. HTTP endpoints are not supported for security reasons.
Webhook notifications contain minimal data (MessageId, MessageType, Timestamp) to:
Always fetch full data via authenticated API calls after receiving webhook notification.
Protect your webhook endpoint:
Error details (stackTrace, callStack) in failed message responses may contain:
Recommendation: Restrict access to error details to authorized personnel only.
Respond to webhooks within 30 seconds (default timeout):
When fetching response data after webhook notification:
For high-volume scenarios:
Track key metrics:
Codeunit: 65346 Cloud Event Message Events
/// Raised when a cloud event message processing completes successfully
[ExternalBusinessEvent('CloudEventMessageCompleted', 'Cloud Event Message Completed',
'A cloud event message has completed successfully.', EventCategory::"Origo Cloud Event")]
procedure OnCloudEventMessageCompleted(MessageId: Guid; MessageType: Text[250]; Timestamp: DateTime)
/// Raised when a cloud event message processing fails
[ExternalBusinessEvent('CloudEventMessageFailed', 'Cloud Event Message Failed',
'A cloud event message has failed processing.', EventCategory::"Origo Cloud Event")]
procedure OnCloudEventMessageFailed(MessageId: Guid; MessageType: Text[250]; Timestamp: DateTime)
/// Raised before a cloud event message starts processing
[IntegrationEvent(false, false)]
internal procedure OnBeforeCloudEventMessageProcessing(var CloudEventMessage: Record "Cloud Event Message")
/// Raised after a cloud event message completes successfully
[IntegrationEvent(false, false)]
internal procedure OnAfterCloudEventMessageCompleted(var CloudEventMessage: Record "Cloud Event Message")
/// Raised after a cloud event message processing fails
[IntegrationEvent(false, false)]
internal procedure OnAfterCloudEventMessageFailed(var CloudEventMessage: Record "Cloud Event Message"; ErrorText: Text)
EnumExtension: 65300 Cloud Event Category
enumextension 65300 "Cloud Event Category" extends EventCategory
{
value(65300; "Origo Cloud Event")
{
Caption = 'Origo Cloud Event';
}
}
© 2024 Origo. All rights reserved.
© Origo – Cloud Events Base Extension