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.
How access works for every connector
Section titled “How access works for every connector”- Secrets live in Infisical as one JSON blob at
erp_connections.secrets_path(packages/integrations/src/auth/credential-service.ts). Possible keys areusername,password,clientId,clientSecret,refreshTokenandapiKey. Older rows hold an encrypted password inline inerp_connections.passwordinstead, 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 identityconnection_<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 anincompleteReasonrefusecreateOrderandupdateOrdereven 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).
Status at a glance
Section titled “Status at a glance”| 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 |
Epicor Kinetic (epicor_kinetic)
Section titled “Epicor Kinetic (epicor_kinetic)”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.
-
Create an access scope in Access Scope Maintenance. Scope entries use the business-object name with no
Svcsuffix. Epicor rejectsErp.BO.SalesOrderSvcas “not valid” but acceptsERP.BO.SalesOrder. -
Add these services to it:
Scope entry Why Calls ERP.BO.CustomerCustomer and ship-to lookup. Ship-tos come from CustomerSvc/ShipToes, notShipToSvcGET Erp.BO.CustomerSvc/Customers,/ShipToesERP.BO.PartItem lookup GET Erp.BO.PartSvc/PartsERP.BO.SalesOrderPO check, dedupe, read-back, and all writes GET SalesOrders,SalesOrders(...)?$expand=OrderDtls; POSTSalesOrders, POSTOrderDtls; PATCHSalesOrders(...); POST, PATCH, DELETEOrderDtls(...)ICE.BO.BAQDesignerOnly to create our OM_BRONZE_*BAQs. Can be removed afterwardsIce.BO.BAQDesignerSvcGetByID,GetFieldList,UpdateICE.BO.AccessScopeOptional. Lets our key re-add a service after a database rebuild without an RDP session Ice.BO.AccessScopeSvcGetByID,UpdateBaqSvcneeds no entry; it is not a service class. A stricter tenant may requireICE.BO.DynamicQueryfor BAQ runs (inferred). -
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. -
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.
-
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). -
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 withtools/epicor/create_baq.pyin 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.
Prophet 21 (prophet21)
Section titled “Prophet 21 (prophet21)”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/formfor the order-acknowledgement PDF. Exact window permissions and licensing are not documented (inferred). - SQL mode only: a dedicated login with
SELECTon the 28dbotables listed inapps/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.
Eclipse E4 / EPMS (eclipse_e4)
Section titled “Eclipse E4 / EPMS (eclipse_e4)”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; writesSubmitOrder,UpdateOrder,ConvertEstimateToOrder. - A read-only SQL login.
apps/dagster/docs/erp/eclipse-e4-production-auth.mdgrants six tables, but the code also readsOrderUDFields,EstimateHeader,EstimateComponent,EstimateQtyTable,OrderQtyTable,MaterialOnHand,Shipment,TemplateHeader, anddbo.MaterialInventoryon databaseEPMS. 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.
WhereFour (wherefour)
Section titled “WhereFour (wherefour)”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.
Acumatica (acumatica)
Section titled “Acumatica (acumatica)”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),
SalesQuoteandConvertToOrdermapped.SalesPricesInquiry(AR202000) for pricing. - Entities: Customer, StockItem, NonStockItem, SalesPricesInquiry, SalesOrder, Invoice,
Warehouse, Branch. Writes: PUT
SalesOrder, POSTSalesQuote/ConvertToOrder, PUTStockItem.
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.
Dynamics 365 Business Central (dynamics)
Section titled “Dynamics 365 Business Central (dynamics)”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.
NetSuite (netsuite)
Section titled “NetSuite (netsuite)”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.
Odoo (odoo)
Section titled “Odoo (odoo)”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.
Dynamics GP (dynamics_gp)
Section titled “Dynamics GP (dynamics_gp)”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.
ePS, Epicor portfolio and INxSQL
Section titled “ePS, Epicor portfolio and INxSQL”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 PostgreSQLepaceon port 5432. - Tharstern.
GET authentication/GenerateAPIToken?email&password&applicationId, which puts the password in the query string. Needs anapplicationIdfrom 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 sendPATCH purchase-orders/{po}/lines/{line}. It refuses while the registry marks INxSQL incomplete, even witherp_inxsql_writes_v1on.
API-key-only types (Eagle, Vision, Propello, LumberTrack) cannot sync in Dagster today, because Dagster requires a username and password in Infisical.