Skip to content

ERP access requirements

This page lists, for every connector registered as kind: 'production' in packages/contracts/src/erp.ts, what a customer has to give us before the connector works: credentials, network path, and ERP-side permissions or API scopes. It also lists every service, endpoint or table our code calls, so a customer’s IT team can scope an API user or key to exactly that and no more.

Built from the code on 2026-10-24. Items marked inferred come from vendor conventions, not from our code or a customer confirming them. When you add an adapter, or a call to a new endpoint, update its section here in the same PR. A scope that is missing one service fails as a 401 or 403 at runtime, usually on a customer’s live order.

  • Secrets live in Infisical as one JSON blob at erp_connections.secrets_path (packages/integrations/src/auth/credential-service.ts). Possible keys are username, password, clientId, clientSecret, refreshToken and apiKey. Older rows hold an encrypted password inline in erp_connections.password instead, and Dagster rejects those.
  • Non-secret settings live in erp_connections.extra_config (plaintext jsonb). Never put a key or token there. A few Dagster sources still read secrets from it; those are called out below as gaps.
  • Writes are gated per connection by the type’s writeFlag, read through Flagsmith identity connection_<uuid> and off by default except Prophet21’s kill switch (apps/webapp/src/services/erp/write-kit.ts). A connection-level identity override beats any segment, so check for one before adding a segment. Types with an incompleteReason refuse createOrder and updateOrder even with the flag on (apps/webapp/src/services/erp/erp-service-factory.ts).
  • Creating a connection in the app only works for prophet21, netsuite, acumatica, wherefour, eclipse_e4, dynamics, odoo and epicor_kinetic (apps/webapp/src/lib/erp/create-erp-connection.ts). The connection test throws “Unsupported ERP system” for everything else, so those rows are inserted by hand.
  • Network. We have no published egress IP. A customer who wants to allowlist us, or whose ERP is on-prem, needs a tunnel. Eclipse E4 is the only connector with tunnel support in code (Cloudflare Tunnel plus Access).
Type Reads Writes Write flag Customer grants
prophet21 Live Live, kill switch defaults on p21_order_writes_v1 API user, OData and Data Services views, or a SQL login
eclipse_e4 Live Live eclipse_e4_order_submission_v1, eclipse_e4_order_update_v1, eclipse_e4_quote_convert_v1 EPMS Connect user, SQL login, tunnel
wherefour Live Live wherefour_order_submission_v1 REST API key and username
epicor_kinetic Live on staging Gated, not yet proven live erp_kinetic_writes_v1 API key with an access scope, Epicor user, network path
acumatica Code complete, no live connection Gated erp_acumatica_writes_v1 OAuth app or user login, endpoint mapping
dynamics (Business Central) Code complete, onboarding broken Ungated none Entra app registration, BC permission sets
netsuite Not working (auth) Ungated none OAuth 2.0 integration record, role
odoo Test instance only Gated odoo_order_writes_enabled User with Sales rights, custom module
dynamics_gp None Never enabled gp_econnect_order_write_v1 eConnect user over an HTTPS proxy
ePS: pace, tharstern, radius, cbs, paxis Unverified Refused (incompleteReason) erp_<type>_writes_v1 Unknown
Epicor portfolio and inxsql Unverified Refused (incompleteReason) erp_<type>_writes_v1 Unknown

The first connector that uses an API key with a vendor-side scope.

Credentials. Every call sends both x-api-key: <apiKey> and Authorization: Basic <username>:<password> (kinetic-rest-client.ts). Store username, password and apiKey in Infisical. Put the company id in extra_config.epicor_company. api_url is https://<host>/<instance>. The client builds /api/v2/odata/<company> from it and only speaks https. The connection form has no field for the API key or company, so set them through the API.

Network. On-prem behind the customer’s firewall. Bison’s path is a Cloudflare Tunnel on a Bison-side box. The webapp has no TLS override, so a self-signed Epicor certificate needs the tunnel ingress set to noTLSVerify or our CA in NODE_EXTRA_CA_CERTS. Dagster accepts extra_config.ca_bundle or insecure_tls.

Epicor setup the customer does.

  1. Create an access scope in Access Scope Maintenance. Scope entries use the business-object name with no Svc suffix. Epicor rejects Erp.BO.SalesOrderSvc as “not valid” but accepts ERP.BO.SalesOrder.

  2. Add these services to it:

    Scope entry Why Calls
    ERP.BO.Customer Customer and ship-to lookup. Ship-tos come from CustomerSvc/ShipToes, not ShipToSvc GET Erp.BO.CustomerSvc/Customers, /ShipToes
    ERP.BO.Part Item lookup GET Erp.BO.PartSvc/Parts
    ERP.BO.SalesOrder PO check, dedupe, read-back, and all writes GET SalesOrders, SalesOrders(...)?$expand=OrderDtls; POST SalesOrders, POST OrderDtls; PATCH SalesOrders(...); POST, PATCH, DELETE OrderDtls(...)
    ICE.BO.BAQDesigner Only to create our OM_BRONZE_* BAQs. Can be removed afterwards Ice.BO.BAQDesignerSvc GetByID, GetFieldList, Update
    ICE.BO.AccessScope Optional. Lets our key re-add a service after a database rebuild without an RDP session Ice.BO.AccessScopeSvc GetByID, Update

    BaqSvc needs no entry; it is not a service class. A stricter tenant may require ICE.BO.DynamicQuery for BAQ runs (inferred).

  3. A scope grants services, not verbs. For a read-only key, restrict the Methods node on ERP.BO.SalesOrder. Which Methods entries map to OData POST, PATCH and DELETE is not yet known.

  4. Create the API key in API Key Maintenance: assign the scope, set the owning company and an expiry. The key is shown once; save it to 1Password and Infisical straight away.

  5. The Epicor user needs company access and sales-order entry rights. We send no Plant, so orders land in that user’s default site (inferred).

  6. The eight BAQs Dagster reads must exist and be runnable by that user: OM_BRONZE_CUSTOMERS (Customer), OM_BRONZE_SHIPTOS (ShipTo), OM_BRONZE_PARTS (Part, Part_UD), OM_BRONZE_XREFS (CustXPrt), OM_BRONZE_ORDERS (OrderHed), OM_BRONZE_ORDER_LINES (OrderDtl), OM_BRONZE_ORDER_RELS (OrderRel), OM_BRONZE_INVOICES (InvcHead). We create them with tools/epicor/create_baq.py in the workbench repo. A customer database rebuild deletes them.

Gaps. Quotes (Erp.BO.QuoteSvc) are not supported. Dagster reads the webapp’s api_url and extra_config.epicor_company (#4498), but a sync also needs the connection’s dataPipeline.paused cleared.

Credentials. POST {api_url}/api/security/token with username and password as headers returns a bearer token (packages/integrations/src/auth/erp-auth.ts). Both live in Infisical. In SQL mode (extra_config.access_method = "sql") Dagster instead reads extra_config.database_config, with the login in Infisical at extra_config.databaseSecretsPath.

Network. The customer’s public HTTPS P21 URL. On-prem tenants need a firewall rule; there is no tunnel path.

P21 setup the customer does.

  • An API user with the “API User” role.
  • Application Security: “Allow OData API Service” = Yes.
  • Dataservice permission for each view we read: p21_view_customer, ship_to, address, shipping_address, inv_mast, inv_xref, inv_loc, oe_hdr, oe_line, the quote views, po_hdr, po_line, invoice_hdr, invoice_line, cashreceipts_hdr, supplier.
  • Access to the Transaction API (POST /uiserver0/api/v2/transaction), used for order, quote, item and PO writes, and the Interactive API plus /api/document/generate/form for the order-acknowledgement PDF. Exact window permissions and licensing are not documented (inferred).
  • SQL mode only: a dedicated login with SELECT on the 28 dbo tables listed in apps/dagster/docs/erp/prophet21-sql-access.md, and nothing else.

Write flags. createOrder, updateOrder and approveQuote read p21_order_writes_v1. Unlike every other write flag it defaults on, because P21 wrote live before it had a gate, so it works as a kill switch: turn it off for one connection to stop that tenant’s order writes. createQuote (p21_quote_create_v1) and updatePurchaseOrder (p21_purchase_order_update_v1) are gated and default off.

Gaps. The ack PDF re-saves the order, so P21 records our API user as last_maintained_by. /uiserver0 is hardcoded.

Credentials. SOAP POST to {api_url}/Service.asmx with a <Credentials> block from Infisical. With eclipse_e4_network_trust_auth_v1 on, the block is empty and EPMS Connect trusts the tunnel. Cloudflare Access headers come from EPMS_CF_ACCESS_CLIENT_ID and EPMS_CF_ACCESS_CLIENT_SECRET. SQL reads use extra_config.database_config with the login in Infisical at databaseSecretsPath.

Network. Customer IIS behind a Cloudflare Tunnel plus Access. MSSQL is reachable over WARP from the temporal-worker VM only; Cloud Run has no route to it.

Customer grants.

  • An EPMS Connect user, separate from E4 UI logins. Required rights are not documented.
  • SOAP calls: reads GetInventoryStatus, GetSummaryJobStatus, GetOrderDetails, GetCustomerList; writes SubmitOrder, UpdateOrder, ConvertEstimateToOrder.
  • A read-only SQL login. apps/dagster/docs/erp/eclipse-e4-production-auth.md grants six tables, but the code also reads OrderUDFields, EstimateHeader, EstimateComponent, EstimateQtyTable, OrderQtyTable, MaterialOnHand, Shipment, TemplateHeader, and dbo.MaterialInventory on database EPMS. Ask for all fifteen.

Gaps. ConvertEstimateToOrder is gated by its own flag, eclipse_e4_quote_convert_v1, which defaults off. The ATP check in apps/temporal-worker/activities/inventory.py uses env-var SQL credentials (VMS_MSSQL_*), not the connection’s. extra_config.tunnel_token is stored in plaintext and never read.

Credentials. Basic auth, username plus the REST API key as the password. Pricing tiers are scraped through a web login with the connection username and the global WHEREFOUR_SCRAPE_PASS.

Network. Public SaaS at my.wherefour.com/api/v1. We retry 429s.

Customer grants. A REST API key, and a web login if we scrape pricing. The WhereFour role the key needs is not documented. Calls: GET /customers, /inventory, /inventory/{id}/stocks, /locations/search, /orders, /customers/{id}/orders, shipments, invoices; POST /customers/{id}/orders; PATCH /customers/{cid}/orders/{oid}, which can delete lines through _destroy.

Credentials. OAuth client_credentials at {site}/AcumaticaERP/oauth2/token, scope api, when Infisical has clientId and clientSecret. Otherwise a cookie login with username, password, company and branch, which uses a licensed API session.

Customer grants.

  • For OAuth, a Connected Application (SM303010).
  • Roles covering AR303000, IN202500, IN202000, SO301000, CR304500 and AR301000 (inferred).
  • On the Web Service Endpoint (SM207060), SalesQuote and ConvertToOrder mapped. SalesPricesInquiry (AR202000) for pricing.
  • Entities: Customer, StockItem, NonStockItem, SalesPricesInquiry, SalesOrder, Invoice, Warehouse, Branch. Writes: PUT SalesOrder, POST SalesQuote/ConvertToOrder, PUT StockItem.

Gaps. No live connection yet. Dagster reads the webapp’s acumatica_* keys and the OAuth client from Infisical; older rows with unprefixed keys or extra_config.oauth_client_* still work.

Credentials. Entra client_credentials at login.microsoftonline.com/{tenant}/oauth2/v2.0/token, scope https://api.businesscentral.dynamics.com/.default. clientId and clientSecret in Infisical; tenant_id, environment and default_company_id in extra_config. api_url is ignored.

Customer grants. An Entra app registration with the Business Central application permission API.ReadWrite.All and admin consent, plus a Microsoft Entra Application card in BC with permission sets covering customers and items (read, item modify) and sales orders and quotes (insert, modify). The exact permission set is inferred. API v2.0 calls: GET customers, items, salesOrders; POST salesOrders, salesOrders({id})/salesOrderLines, salesQuotes({id})/Microsoft.NAV.makeOrder; PATCH salesOrders; POST and PATCH items.

Gaps. Writes have no flag. The connection test calls Dataverse WhoAmI, which is the wrong product, and the create flow stores neither the client secret nor tenant_id.

Credentials. OAuth 2.0 against {apiUrl}/services/rest/auth/oauth2/v1/token, by refresh token or a JWT-bearer grant. consumerKey, consumerSecret, accountId and refreshToken in Infisical.

Customer grants (inferred). REST Web Services and “Log in using OAuth 2.0 Access Tokens” enabled, an integration record, and a role with Customers view, Items create and edit, and Sales Order create and edit. Calls: REST Record v1 customer, item, salesOrder.

Gaps. Our JWT is signed HS256 with the consumer secret; NetSuite machine-to-machine expects a certificate-signed JWT, so this likely fails. The form asks for TBA tokenId and tokenSecret, which nothing uses. Writes have no flag. No active connections.

Credentials. XML-RPC authenticate with a username and a password or API key from Infisical. Database from extra_config.database_name.

Customer grants. A user with Sales / User: All Documents, contact creation, and product and UoM read (inferred), plus our erp_unlocked module, which grants CRUD on product.customer.info. Models: res.partner, product.product, product.template, uom.uom, res.country, sale.order, sale.order.line.

Write flag. odoo_order_writes_enabled gates createOrder, updateOrder and approveQuote, and defaults off.

Gaps. No Dagster source.

Writes only, through eConnect CreateTransactionEntity with Basic auth over an HTTPS proxy in front of the customer’s eConnect service. The customer would grant a Windows or SQL identity that eConnect accepts (DYNGRP or an eConnect service user, inferred). No reads, no Dagster source, and no caller ever sets gpEconnectWriteEnabled, so every write is disabled.

pace, tharstern, radius, cbs, paxis, the epicor_* portfolio types and inxsql all carry an incompleteReason. Their endpoint paths are unverified against a live system, and we have no confirmed list of vendor permissions for any of them. Before onboarding one, confirm the auth flow and permissions with the vendor and replace this section with a real one.

What the code calls today:

  • Pace. SOAP {apiUrl}/rpc/services/{Service} with Basic auth. Needs a Pace system user with “Allow remote API usage”. Dagster can instead read PostgreSQL epace on port 5432.
  • Tharstern. GET authentication/GenerateAPIToken?email&password&applicationId, which puts the password in the query string. Needs an applicationId from the vendor.
  • Radius, CBS, Paxis, and the Epicor portfolio REST types. API key or Basic auth plus a vendor header (plant, company, tenant or warehouse id). Dagster reads their SQL or ODBC tables with the same username and password.
  • INxSQL. The Supplymatic PO apply (apps/webapp/src/lib/revision/targets/inxsql-purchase-order.ts) would send PATCH purchase-orders/{po}/lines/{line}. It refuses while the registry marks INxSQL incomplete, even with erp_inxsql_writes_v1 on.

API-key-only types (Eagle, Vision, Propello, LumberTrack) cannot sync in Dagster today, because Dagster requires a username and password in Infisical.