Skip to content
Private BetaGet Early Access Get Early Access

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.

Updated 2026-08-246 min read

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

  1. Install the module in an INTERA trial or developer environment.
  2. Create an Integration Instance and fill in the three configuration fields.
  3. Call the DataSource and check the response.
  4. 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, name and kind — use sql, file or legacy where the source is not an API;
  • replace configSchema with the fields your system needs to connect;
  • replace callBillingApi with 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.

Naming changed in v0.003
KPI is now Metric across the product and the docs. Existing roles were migrated automatically; saved dashboards keep working.