API Access in Apps

How Operator Apps talk to ERP.net and other services

Apps built with the App Builder can read and write ERP.net data through the ERP.net Domain API, and they can also call external REST APIs when needed. This topic explains how both work, what limits apply, and how to handle errors.

What Apps Can Do with ERP.net Data

Apps built with the App Builder have access to the ERP.net Domain API (OData v4), enabling them to:

Data Volume Limits

App Builder apps are lightweight client-side applications with strict data limits to ensure performance and stability:

If your use case requires processing larger volumes of data, consider these strategies:

Domain API Transactions

The ERP.net Domain API allows only one open transaction per session. If an app opens a transaction (for a multi-step write that must be atomic) and fails to close it after an error, the session becomes unusable for any further transactional work until it expires — every later attempt fails with "Maximum allowed transactions for this session have been reached." The App Builder is instructed to avoid transactions when a single write would do, and — when one is genuinely needed — to wrap it in a try / catch / finally block so the transaction is always rolled back on error. You don't need to ask for this behavior; the App Builder applies it automatically. If you ever see this error from an older app, regenerate or patch it in the App Builder to pick up the safer pattern.

Error Handling for ERP.net Calls

When window.operator.fetch(...) fails, the rejected Error carries the exact ERP.net server message, so apps should surface it to the user instead of a generic "failed" toast. The error object includes:

Apps generated by the App Builder follow this pattern automatically:

try {
  await window.operator.fetch('/Production_Technologies_RecipeIngredients', {
    method: 'POST',
    body: payload,
  });
} catch (e) {
  if (e.isErpError) {
    toast.error('ERP.net rejected the change', { description: e.message });
  } else {
    toast.error('Could not reach ERP.net', { description: e.message });
  }
}

Most "the app can't save to ERP.net" reports are in fact ERP.net permission errors — the connected user lacks read or write access on a referenced entity. The full error is also recorded in the app's runtime logs (visible to the App Builder agent), which usually points to the exact missing permission.

The same e.message also carries proxy-side reasons such as "Your connection to ERP.net expired and could not be refreshed. Please reconnect the instance." or "No ERP.net instance is currently selected." — apps do not need any special-case code; surfacing e.message is always meaningful. These improvements apply live to every existing app on the next page reload — no rebuild or App Builder edit is required.

Automatic request serialization

ERP.net rejects concurrent calls from the same session with HTTP 429. To prevent this, Operator automatically serializes every window.operator.fetch() call inside the app — even if the app code accidentally fires them in parallel (e.g. Promise.all([...])), Operator queues them and sends one at a time. App authors should still write sequential await code for clear loading and error states, but this safety net protects existing apps from accidental 429s with no rebuild needed.

Connection validation on app start

When a user opens an app at /app/:slug, Operator validates the active ERP.net connection before mounting the app iframe. If the access token is expired, Operator silently attempts to refresh it. If the refresh fails, the app does not load — instead, Operator displays a full-screen "Session expired for {instance}" overlay with a Reconnect button that walks the user through the ERP.net sign-in flow. As soon as sign-in completes, the iframe boots with a fresh token and the app runs normally. App authors do not need to handle this case in code.

External API Access

App Builder apps can call external REST APIs directly using the standard browser fetch() API — no special SDK needed. This is useful for integrating third-party services, public data sources, or custom backends alongside your ERP data.

How It Works

Use fetch() as you would in any web application:

const response = await fetch("https://api.example.com/data", {
  headers: { "Authorization": "Bearer YOUR_TOKEN" }
});
const data = await response.json();

CORS Restrictions

Apps run in a browser sandbox, so standard CORS (Cross-Origin Resource Sharing) rules apply:

const res = await window.operator.corsFetch(
  'https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ countryCode: 'BG', vatNumber: '121084091' }),
  }
);
if (res.ok) console.log(res.json());

corsFetch is not an open proxy:

Tips