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:
- Query data — Fetch and display records from any ERP entity
- Create records — Build forms that create new entries in the system
- Update records — Modify existing data with validation
- Display dashboards — Visualize data with charts, tables, and summary cards
- Filter and search — Add interactive controls for data exploration
- Respond to context — React to the connected instance and enterprise company
- Call AI — Invoke Pure AI for one-shot completions or run full Operator agents (built-in or user-defined) with tools and capabilities. See AI in Apps.
Data Volume Limits
App Builder apps are lightweight client-side applications with strict data limits to ensure performance and stability:
- Maximum 1,000 rows per request — Each API call can return at most 1,000 rows. Always apply filters to keep result sets within this limit.
- Maximum 10,000 rows in memory — The app should never accumulate more than 10,000 rows across all loaded data sets combined.
If your use case requires processing larger volumes of data, consider these strategies:
- Narrower filters — Use date ranges, status filters, or other criteria to reduce the data set.
- Paginated views — Load one page of data at a time without keeping all pages in memory.
- Summary queries — Use aggregate or summary data instead of loading raw detail rows.
- Server-side processing — Offload heavy data processing to the ERP system rather than pulling everything into the browser.
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:
error.message— the ERP.net server message (already in the instance's language, e.g. *"Вие нямате достъп до обект Gen_Products(**73d7a)").error.isErpError—truewhen ERP.net rejected the request,falsefor transport/authentication failures.error.status— HTTP status returned by ERP.net (e.g.403,500).error.statusText— friendly label (e.g. "ERP.net permission denied", "ERP.net rejected the request").error.code,error.type,error.details— when ERP.net provides them.
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:
- If the external API includes an
Access-Control-Allow-Originheader, you can call it directly withfetch(). - If it does not (e.g. EU VIES, many government APIs), the browser blocks the response. For these cases use
window.operator.corsFetch(url, options)— Operator forwards the request server-side and returns the response wrapped in CORS headers your app can read.
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:
- Only signed-in Operator users can call it.
- Limited to 60 requests / minute / user.
- Sensitive headers (
Authorization,Cookie) are stripped before forwarding, so never use it for APIs that require a secret API key — build a dedicated backend function so the secret stays server-side. - Internal hosts (
*.erp.net,*.supabase.co,*.lovable.app,localhost, private IPs) are blocked.
Tips
- Public APIs with CORS (weather, exchange rates) — call directly with
fetch(). - Public APIs without CORS — call through
window.operator.corsFetch(). - APIs that need a secret key — neither path is safe in client code; ask for a dedicated server function.
- ERP.net data should always be accessed through
window.operator.fetch(), which handles authentication and proxying automatically. - Sequential requests — As with ERP API calls, avoid concurrent requests to external APIs. Use
awaitfor each call to prevent rate limiting or ordering issues.