Sample integration code
A complete, minimal INTERA Integration Module in JavaScript: manifest, configuration schema, one read-only DataSource, error handling and how to run it.
This page contains a complete, working example of a small INTERA Integration Module. It is deliberately minimal: one manifest, one configuration schema, one read-only DataSource, and clear errors. Copy it, replace the call to the external system with your own, and you have a valid module INTERA can load, configure and call.
The rules the example follows are described in Integration Contract; the schemas and error codes it uses are listed in Reference. If you have not built an integration before, read Build Your First Integration first.
What the example does
The module connects to an imaginary billing API over HTTPS with an API key, and exposes one DataSource called invoice_summary. Given a period, the DataSource returns the number of open and overdue invoices and the overdue amount — enough for a Metric Widget on a finance Dashboard.
It is read-only. It declares no write-back actions and no entity synchronisation, which is the right starting point for a first integration.
Project layout
example-billing/
├── src/
│ └── index.js
├── package.json
└── README.md
{
"name": "example-billing",
"version": "1.0.0",
"main": "src/index.js",
"license": "UNLICENSED"
}
The main entry point exports the integration definition: a manifest, a configuration schema and a list of DataSources.
The manifest
The manifest tells INTERA what the module is, what it can provide and how long its results may be cached.
const manifest = {
id: "example_billing",
name: "Example Billing",
version: "1.0.0",
kind: "readonly_api",
capabilities: ["datasource"],
defaultCacheTtlSec: 300,
description: "Open and overdue invoice totals from the Example Billing API.",
vendor: "Example Software Ltd",
maintainer: "integrations@example.com"
};
The id must be unique and stable across versions. Raise version whenever the package changes.
Configuration schema
The configuration schema describes what an administrator must supply to create an Integration Instance from this module. Credential values belong to the instance, never to the module — mark them secret so INTERA stores and masks them accordingly.
const configSchema = [
{
key: "baseUrl",
label: "API Base URL",
type: "string",
required: true,
default: "https://api.example.test"
},
{
key: "apiKey",
label: "API Key",
type: "secret",
required: true
},
{
key: "companyId",
label: "Company ID",
type: "string",
required: false
}
];
The DataSource
A DataSource is a named, on-demand endpoint. It declares the parameters it accepts, and its getData function returns a predictable structure — the same shape every time, including when there is nothing to report.
const invoiceSummary = {
id: "invoice_summary",
name: "Invoice Summary",
description: "Open and overdue invoice totals for a period.",
cacheTtlSec: 300,
paramsSchema: [
{ key: "from", label: "From", type: "date", required: true },
{ key: "to", label: "To", type: "date", required: true }
],
async getData(params, context) {
const { from, to } = params;
if (!from || !to) {
throw integrationError({
code: "DATASOURCE_INVALID_PARAMS",
message: "Both 'from' and 'to' dates are required.",
retryable: false
});
}
const payload = await callBillingApi("/invoices/summary", { from, to }, context);
return {
generatedAt: new Date().toISOString(),
period: { from, to },
currency: payload.currency || "EUR",
totalOpen: payload.open_count || 0,
totalOverdue: payload.overdue_count || 0,
overdueAmount: payload.overdue_amount || 0,
warnings: []
};
}
};
Note what the response carries besides the numbers: when it was generated, which period it covers, and which currency the amounts are in. A Metric that cannot state its period cannot be trusted on a Dashboard.
Calling the external system
Credentials come from context.config at runtime. Never hard-code them, and never write them into an error message or a log line.
async function callBillingApi(path, query, context) {
const { baseUrl, apiKey, companyId } = context.config || {};
if (!baseUrl || !apiKey) {
throw integrationError({
code: "CONFIG_MISSING_REQUIRED_FIELD",
message: "API Base URL and API Key are required for this Integration Instance.",
retryable: false
});
}
const url = new URL(path, baseUrl);
Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, value));
if (companyId) url.searchParams.set("company_id", companyId);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15000);
let response;
try {
response = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
signal: controller.signal
});
} catch (error) {
throw integrationError({
code: "CONNECTION_TIMEOUT",
message: "The Example Billing API did not respond within 15 seconds.",
retryable: true
});
} finally {
clearTimeout(timeout);
}
if (response.status === 401 || response.status === 403) {
throw integrationError({
code: "AUTH_INVALID_CREDENTIALS",
message: "The API key was rejected by the Example Billing API.",
retryable: false
});
}
if (response.status === 429) {
throw integrationError({
code: "RATE_LIMITED",
message: "The Example Billing API rate limit was reached.",
retryable: true
});
}
if (!response.ok) {
throw integrationError({
code: "EXTERNAL_SYSTEM_ERROR",
message: "The Example Billing API returned an unexpected response.",
retryable: true,
details: { statusCode: response.status }
});
}
return response.json();
}
Reporting errors
Every failure gets a stable machine-readable code, a message an administrator can act on, and a retryable flag so INTERA can tell a temporary outage from a misconfiguration.
function integrationError({ code, message, retryable, details }) {
const error = new Error(message);
error.code = code;
error.source = "example_billing";
error.datasource = "invoice_summary";
error.retryable = Boolean(retryable);
error.details = details || {};
error.timestamp = new Date().toISOString();
return error;
}
Which produces, for a rejected key:
{
"code": "AUTH_INVALID_CREDENTIALS",
"message": "The API key was rejected by the Example Billing API.",
"source": "example_billing",
"datasource": "invoice_summary",
"retryable": false,
"details": {}
}
The complete module
Put together, src/index.js exports one object:
module.exports = {
manifest,
configSchema,
dataSources: [invoiceSummary]
};
That is the whole contract for a read-only integration: what the module is, what it needs, and what it can be asked for.
Trying it out
- Install the module in an INTERA trial or developer environment.
- Create an Integration Instance and fill in the three configuration fields.
- Call the DataSource and check the response.
- Bind the result to a Metric, then place that Metric on a Dashboard.
GET /api/data/example_billing/invoice_summary?from=2026-07-01&to=2026-07-31
{
"generatedAt": "2026-07-31T10:30:00Z",
"period": { "from": "2026-07-01", "to": "2026-07-31" },
"currency": "EUR",
"totalOpen": 42,
"totalOverdue": 7,
"overdueAmount": 18425,
"warnings": []
}
Test the failure paths too: a wrong API key must return AUTH_INVALID_CREDENTIALS, an unreachable host CONNECTION_TIMEOUT, and an empty period a valid response with zeroes rather than an error.
Adapting it to your own system
- change the manifest
id,nameandkind— usesql,fileorlegacywhere the source is not an API; - replace
configSchemawith the fields your system needs to connect; - replace
callBillingApiwith your query, file read or client call; - keep the response shape stable, and keep the timestamps and period;
- add a second DataSource only once the first one works end to end.
When the module behaves, continue with Testing & Validation and Packaging & Submission.