Syncing data to external systems with events
The most common integration is a one-way sync: something changes in Twinfinity, and you push that change somewhere else. Twinfinity gives you two pieces to build it — a stream of events delivered to your own Azure Service Bus, and an OAuth client your service can use to read more from the REST API. This page shows how to put them together.
Twinfinity ──(CloudEvent)──► Azure Service Bus ──(trigger)──► your function ──(REST + OAuth)──► your systemPrerequisite — provisioning. Two things are set up per tenant before any of this runs, and both are arranged with Twinfinity / your administrator: (1) a forwarding subscription that pushes the events you care about to your Service Bus queue or topic, and (2) an OAuth client (client id + secret) with permission to read what you need. This page assumes both exist.
1. Receiving events
Twinfinity forwards events to an Azure Service Bus queue or topic that you own. You give Twinfinity the connection details when the subscription is provisioned; from then on, matching events arrive as messages. Anything that can read from Service Bus works — most often an Azure Function with a Service Bus trigger, but a service listening to the Service Bus works equally well.
The event shape
Every message is JSON in the CloudEvents 1.0 format. This is the contract you code against:
{
"specversion": "1.0",
"id": "550e8400-e29b-41d4-a716-446655440000",
"source": "/customers/<customerId>",
"type": "com.twinfinity.issue-topic-upserted",
"time": "2026-03-01T10:30:00Z",
"subject": "issue:topic:abc-123",
"datacontenttype": "application/json",
"data": { "title": "HVAC Review", "priority": "high" },
"twinfinitysubscriptionid": "d4e5f6a7-...",
"twinfinityrelatedentities": ["twin:model:def-456"]
}The fields you will use most:
type— what happened, e.g.com.twinfinity.issue-topic-upserted. Branch on this.subject— the entity that changed, asdomain:entityType:id. The trailing id is what you pass to the REST API.data— a compact, domain-specific payload. Enough to decide what to do; fetch the rest from the API when you need it.id— a stable event id. Also set as the Service BusMessageId, so you get de-duplication and a natural idempotency key.twinfinityrelatedentities— other entities referenced by the change (e.g. the twin an issue is about).
Event types available today
Event type follows the pattern com.twinfinity.<domain>-<entityType>-<action>. The set is growing; the types emitted today are:
Event | Raised when |
|---|---|
| An issue topic is created, edited, removed or restored. |
| A comment on an issue topic changes. |
| A twin's content is updated. |
Your forwarding subscription decides which of these reach you — it can filter by domain (e.g. all issue events), by entity type, by action (e.g. all deleted events), or take everything. You only receive the types you asked for, so write your handler to ignore anything it does not recognise.
Delivery semantics
Near real-time. Events are forwarded shortly after the change is committed.
At-least-once. A given event may be delivered more than once. Make your handler idempotent on
id(the Service BusMessageIdalso lets the broker de-duplicate).Security-trimmed. A subscription runs as a specific Twinfinity identity, and only events that identity is allowed to see are delivered — so you never receive data the integration is not permitted to read.
2. Calling back into the API
Because data is deliberately small, most integrations take the id from the event's subject and call the REST API for the full record. Your service authenticates with the OAuth client credentials grant — a client id and secret, no interactive user — against your tenant's identity provider, and sends the resulting bearer token to the gateway.
From Node / TypeScript with @twinfinity/authentication
The @twinfinity/authentication package implements the client-credentials flow and attaches the token to every request for you. It also refuses to run in a browser, since the secret must stay server-side — which is exactly what you want in a function or lambda.
import {
TwinfinitySession,
TwinfinityHttpClient,
HttpMethod
} from '@twinfinity/authentication';
// Provider discovery, token acquisition and renewal are all handled for you.
const session = await TwinfinitySession.establishWithClientCredentials({
apiUrl: process.env.TWINFINITY_API_URL!, // https://<customer>.twinfinity.com
clientId: process.env.TWINFINITY_CLIENT_ID!,
clientSecret: process.env.TWINFINITY_CLIENT_SECRET!
});
const http = TwinfinityHttpClient.create(
{ clientName: 'my-integration', clientVersion: '1.0.0' },
session
);
// http.fetch() now adds a fresh bearer token to every call.
const response = await http.fetch(HttpMethod.Get, someApiUrl);
// Or use one of the typed clients for simpler integration
const twinClient = new TwinClient(apiUrl, httpClient);
const twin = await twinClient.getTwin({ ... });Putting it together: an Azure Function
A Service Bus trigger gives you the CloudEvent directly. Build the auth client once per instance (tokens refresh on their own) and reuse it:
import { app, InvocationContext } from '@azure/functions';
import {
TwinfinitySession,
TwinfinityHttpClient,
HttpMethod
} from '@twinfinity/authentication';
// Created lazily once, shared across invocations on this instance.
let clientPromise: Promise<TwinfinityHttpClient> | undefined;
function getClient(): Promise<TwinfinityHttpClient> {
return (clientPromise ??= (async () => {
const session = await TwinfinitySession.establishWithClientCredentials({
apiUrl: process.env.TWINFINITY_API_URL!,
clientId: process.env.TWINFINITY_CLIENT_ID!,
clientSecret: process.env.TWINFINITY_CLIENT_SECRET!
});
return TwinfinityHttpClient.create(
{ clientName: 'issue-sync', clientVersion: '1.0.0' },
session
);
})());
}
app.serviceBusQueue('issueSync', {
connection: 'TWINFINITY_EVENTS', // app setting: your Service Bus connection
queueName: 'twinfinity-events',
handler: async (message: unknown, _context: InvocationContext) => {
const event = message as { type: string; subject: string; id: string };
// Only act on the events this function cares about.
if (!event.type.startsWith('com.twinfinity.issue-topic-')) return;
// subject is "issue:topic:<id>" — pull the id and read the full topic.
const topicId = event.subject.split(':').pop();
const http = await getClient();
const res = await http.fetch(
HttpMethod.Get,
`${process.env.TWINFINITY_API_URL}/<path-to-the-resource>/${topicId}` // see the REST API reference
// <!-- TODO: link REST API reference -->
);
const detail = await res.json();
await syncToExternalSystem(detail); // your code
}
});From any other runtime
If your function is not Node, request a token directly from your tenant's token endpoint and send it as a bearer token. The grant is standard OAuth 2.0 client credentials:
curl -X POST "$TOKEN_ENDPOINT" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET"
# then call the API with: Authorization: Bearer <access_token>The token endpoint for your tenant is provided together with the client id and secret at provisioning time. Tokens are short-lived — cache them and re-request shortly before they expire.
What's available today
Delivery target: Azure Service Bus.
Event format: CloudEvents 1.0 JSON.
Events: issue topics, issue comments, and twin content updates (see the table above) — with more domains added over time.
Auth for callbacks: OAuth 2.0 client credentials, usable from
@twinfinity/authenticationor any HTTP client.
In short
Twinfinity forwards CloudEvents to your Azure Service Bus; your function or lambda consumes them.
Branch on
type, take the id fromsubject, and be idempotent onid.Fetch full detail from the REST API using an OAuth client-credentials token — easiest via
@twinfinity/authentication.The Service Bus subscription and the OAuth client are provisioned for you — arrange them with your Twinfinity contact.