Workspace administration and reference data
Use the operations on this page to provision the staff users and API keys an integration needs, to read the settings that change how orders and stock behave, and to cache the reference data your integration uses. For every field and argument, see the API reference.
1. Permissions and who can call what
Every operation on this page is a GraphQL operation that takes a user JWT from a staff sign-in. API keys (gm_live_) authenticate only the /api/service/* REST endpoints, so a key cannot call any operation on this page. Creating and revoking keys also needs a JWT. For how keys are used and rotated, see authentication.
Permissions use the format module:resource:action, for example users:user:create, settings:role:update or audit:activity_log:read. A held permission matches when it is exact, when it is module:* (everything in a module), or when it is module:resource:* (everything on one resource).
The platform wildcards *, *:* and *:*:* are never granted to a tenant role, not even to Owner. The tenant ADMIN role is not a bypass: permission checks look only at the permissions the caller holds. A malformed permission string matches nothing.
A few administrative operations are gated by the caller's user role instead of a permission. The user role is the role field on me. Each such operation is marked below as "Role-gated" with the role values it accepts. The check is an exact match on me.role against that list: a caller whose role is not listed is refused, whatever permissions they hold. OWNER is not accepted unless it is listed, so a user whose role is OWNER cannot call an operation gated to ADMIN or MANAGER.
Operations marked as authenticated only need a valid JWT but no permission. myPermissions, permissionGroups and permissions(module: String, resource: String) are authenticated only, so you can use them to discover what a user can do.
query WhoAmI {
me {
id
email
role
organizationId
permissions
}
}
query MyPermissions {
myPermissions {
id
module
resource
action
direct
fromRole
}
}
Build each permission string from module, resource and action. The permissions list on me is the set the server enforces. Permissions can change without a new sign-in, so re-read them rather than caching them for long. For how a call is refused, see tenancy and permissions.
2. Invite, list, and update staff users
createUser creates a staff user with no password and emails an invite link. Nobody can set another user's password. To send a password reset link, use sendPasswordResetLink.
createUser(input: CreateUserInput!): User!. Requiresusers:user:create.usersFiltered(input: GetUsersInput). Authenticated only.user(id: String!). Authenticated only.updateUser(id: String!, input: UpdateUserInput!). Requiresusers:user:update.deleteUser(id: String!). Requiresusers:user:delete.sendPasswordResetLink(userId: ID!). Requiresusers:user:update.userRoles(userId: String!). Requiresusers:user:read.
mutation InviteUser($input: CreateUserInput!) {
createUser(input: $input) {
id
email
firstName
lastName
status
hasPassword
mustChangePassword
}
}
{
"input": {
"email": "new.user@example.com",
"firstName": "<first>",
"lastName": "<last>",
"roleId": "<role-id>"
}
}
query Users($input: GetUsersInput) {
usersFiltered(input: $input) {
users { id email name status role lastLoginAt }
totalCount
hasNextPage
}
}
{
"input": {
"pagination": { "page": 1, "limit": 50 }
}
}
The rules for staff users:
User.emailis unique per organization, not globally. ARC trims and lower-cases the address before it checks for duplicates, and the check ignores case. A duplicate is refused with the messageUser with email … already exists in this organization.- A phone number must be unique among your organization's staff users, on create and on update. Formatting is ignored when numbers are compared. A duplicate is refused with a message that starts
Phone number … is already used by …. - The new user always joins your credential's organization.
CreateUserInputstill lists anorganizationIdfield, but ARC discards it. Leave it out. CreateUserInputhas no password field. A user created with statusACTIVEgets an invite email with a link valid for 24 hours. A user created asINACTIVEorSUSPENDEDgets no email. After you set them toACTIVE, send the link withsendPasswordResetLink.sendPasswordResetLinksends a user who has no password yet a new 24-hour invite. A user who has a password gets a reset link valid for one hour, and the existing password keeps working until the link is used. A resend within about a minute is refused, and onlyACTIVEusers can be sent a link.- Naming a role on create or update, through
roleIdor theroleenum, also requiressettings:role:update. Without that permission the call is refused, and the role is never silently defaulted. If you name no role, the default role applies. PreferroleId: aroleenum value must match a role in your organization, or the call is refused. - To edit, delete or send a link to another user, you need every permission that user holds. You cannot delete yourself.
UserStatusvalues areACTIVE,INACTIVEandSUSPENDED. The staff values of theUserRoleenum areADMIN,MANAGER,OWNER,SALES_REP,VIEWERandWAREHOUSE_STAFF. The enum also lists buyer and platform values that staff provisioning does not use. Staff operations refuse customer sign-ins.usersFilteredis page-based. Sendpaginationwithpageandlimit, and readtotalCountandhasNextPage. For the shapes ARC uses across queries, see errors, pagination, and idempotency.
3. Roles and assigning them
A role is a named set of permissions. You create and clone roles, grant permissions to a role, and assign users to roles. Seeded system roles are protected.
roles(includeDeleted: Boolean)androle(id: ID!). Authenticated only.createRole(input: CreateRoleInput!). Requiressettings:role:create.cloneRole(input: CloneRoleInput!). Requiressettings:role:create.updateRole(input: UpdateRoleInput!). Requiressettings:role:update.restoreRole(id). Requiressettings:role:update.removeRole(id). Requiressettings:role:delete. ReturnsBoolean!.assignUserToRole(input: AssignUserToRoleInput!). Requiressettings:role:update. ReturnsBoolean!.removeUserFromRole. Requiressettings:role:update.assignPermissionToRole(input: AssignPermissionInput!). Requiressettings:permission:update. ReturnsBoolean!.removePermissionFromRole. Requiressettings:permission:update.updateRolePermissions(roleId: ID!, permissionIds: [ID!]!). Requiressettings:permission:update. ReturnsBoolean!.assignUserPermission(input: AssignUserPermissionInput!)andremoveUserPermission. Requiresettings:permission:update.
The inputs:
CreateRoleInput:nameanddisplayNameare required.description,isActive,metadataandpriorityare optional.CloneRoleInput:sourceRoleId,newRoleNameandnewRoleDisplayNameare required.descriptionis optional,includePermissionsdefaults totrue, andincludeUsersdefaults tofalse.AssignUserToRoleInput:userIdandroleIdare required.expiresAtis optional.AssignPermissionInput:roleIdandpermissionIdare required.conditionsis optional.AssignUserPermissionInput:userIdandpermissionIdare required.granteddefaults totrue.expiresAtandconditionsare optional.
The rules for roles:
- There is no privilege escalation. You can add a permission to a role, or grant it to a user, only if you hold that permission yourself. Cloning a role needs every permission it copies. Platform-reserved permissions are refused even to a holder of a wildcard.
- The Owner role, identified by the name
Owner, always holds every permission and cannot be edited. Its permissions are refused with the messageThe Owner role's permissions cannot be modified. - System roles cannot be modified or deleted, and the refusal reads
System roles cannot be modifiedorSystem roles cannot be deleted. ARC sets the system flag, and you cannot set it. - A role name that already exists is refused. If a soft-deleted role has that name, restore it with
restoreRoleinstead of creating a new one. - Roles are listed for your organization only.
- Removing a permission from a role takes effect for the role's holders without a new sign-in.
mutation AssignRole($input: AssignUserToRoleInput!) {
assignUserToRole(input: $input)
}
{
"input": {
"userId": "<user-id>",
"roleId": "<role-id>"
}
}
query Roles {
roles {
id
name
displayName
isSystem
isActive
permissionCount
userCount
}
}
To list the roles one user holds, query userRoles with the user's id:
query UserRoles {
userRoles(userId: "<user-id>") {
id
name
displayName
}
}
4. API keys: create, list, revoke
This section covers the GraphQL side of the key lifecycle. For the key format, the meaning of READ_ONLY and READ_WRITE, and the steps to rotate a key, see authentication. ARC has no rotate mutation. To rotate, create a new key, deploy it, then revoke the old key's id.
createApiKey(input: CreateApiKeyInputGql!): ApiKeyResult!. Role-gated:ADMINorMANAGER. If API access is not enabled on your organization's plan, the call is refused. Ask your Growmax account manager to enable it.apiKeysandapiKey(id: String!). Requiresettings:api:readorsettings:tenant:read.revokeApiKey(id: String!): Boolean!. Role-gated:ADMINorMANAGER. Also refused when API access is not enabled on the plan.
The rules for keys:
CreateApiKeyInputGqlrequiresname.scopeisREAD_ONLYby default, orREAD_WRITE.expiresAtmust be in the future.expiresInDaystakes 1 to 3650, andexpiresAtwins when you send both.allowedIpstakes IPv4 addresses, IPv6 addresses or CIDR ranges, and an empty list allows any address.rateLimitis the key's requests per minute: an integer from 1 to 10000, 100 by default. ARC enforces it per key on every/api/service/*request and answers429with codeAPI_KEY_RATE_LIMITEDwhen it is used up. It cannot be changed after creation; create a replacement key instead. See rate limits.ApiKeyResult.keyis returned only in thecreateApiKeyresponse.maskedKey,keyPrefix,lastUsedAt,usageCount,revokedAtandisActiveare for listing keys. The full key cannot be read back later.- Revoking a key that is already revoked fails with the message
API key already revoked. Revocation is permanent. - The role gate applies to the caller. Holding a settings API permission is not enough to create or revoke keys:
me.rolemust beADMINorMANAGER.
mutation CreateKey($input: CreateApiKeyInputGql!) {
createApiKey(input: $input) {
id
name
key
keyPrefix
scope
expiresAt
allowedIps
rateLimit
}
}
{
"input": {
"name": "erp-sync",
"scope": "READ_ONLY",
"expiresInDays": 90,
"rateLimit": 120
}
}
query Keys {
apiKeys { id name keyPrefix maskedKey scope rateLimit isActive lastUsedAt usageCount revokedAt expiresAt }
}
mutation RevokeKey {
revokeApiKey(id: "<api-key-id>")
}
5. Organization and tenant settings that change integration behaviour
Several tenant settings change how orders, stock, tax and currency behave for an integration. Before you change a setting, read it with tenantSettingsByCategory and reuse the category and key it returns. ARC records each change to a setting's value, and tenantSettingHistory(category: String!, key: String!, limit: Int) returns that history for one setting.
organization and updateOrganization take an id argument only for schema compatibility. It never selects a tenant: pass the organizationId that me returns, and any other value is refused.
organization(id: String!): Organization!. Authenticated only. Any id other than your own is refused withUnauthorized to view this organization.updateOrganization(id: String!, input: UpdateOrganizationInput!): Organization!. Requiressettings:organization:update.updateInventorySettings(input: InventorySettingsInput!). Requiressettings:inventory:updateorsettings:tenant:update.tenantSettingsByCategory(category: String!)andtenantSettingHistory. Requiresettings:tenant:read.updateTenantSetting(input: UpdateTenantSettingInput!)andbulkUpdateTenantSettings. Requiresettings:tenant:update.salesPolicyreads the sales policy. It requires any ofsettings:tenant:read,sales:quote:create,sales:quote:update,sales:sales_order:create,sales:sales_order:updateorsales:sales_order:ship.updateSalesPolicy(input: UpdateSalesPolicyInput!)changes it. Requiressettings:sales:updateorsettings:tenant:update.organizationTaxConfig. Authenticated only.createOrUpdateOrganizationTaxConfig(input: CreateOrganizationTaxConfigurationInput!). Role-gated:ADMINorMANAGER.
The rules for settings:
- Allow Negative Stock is the
allow_negative_stockkey in the inventory settings category.updateInventorySettingstakesallowNegativeStockand writes it to that key. When the setting is off, confirming a sales order (fromDRAFTorAWAITING_PAYMENTtoCONFIRMED) is refused if on-hand stock cannot cover it. Variants that do not track stock pass. When the setting is on, the order confirms and the shortfall is recorded as a backorder. The buyer storefront cart reads the same setting. A confirmed order does not mean that stock is reserved. For the confirmation flow, see quote-to-cash. - The other inventory inputs include
autoReorderPoint,costingMethod,defaultUom,defaultWarehouse,enableBatchTracking,enableExpiryTracking,enableSerialTracking,lowStockThreshold, and more. After you changedefaultWarehouse, thedefaultWarehousequery returns the new value. - Base currency is changed through
updateOrganization(itscurrencyfield) or throughsetBaseCurrency, which is described in section 6. ARC refuses the change once posted transactions exist or once an administrator has locked the base currency. When transactions exist, the refusal readsCannot change base currency: N transaction(s) exist. Draft and cancelled documents do not count. Treat base currency as fixed after go-live. - ARC never converts a price from one currency to another. A line in a foreign currency is priced only from a price stored in that currency, and it is refused when no such price exists. For the pricing waterfall and document currency, see money, pricing, and tax.
defaultTaxCalculationholds the tenant's default tax calculation method. Documents carry their owntaxCalculationMethod. Quote conversion refuses a taxed line that has no valid method, and it never falls back toEXCLUSIVE. The enum listsADDITIVE,COMPOUND,EXCLUSIVEandINCLUSIVE, but new quotes, sales orders and invoices areEXCLUSIVE-only: sendEXCLUSIVEor omit the method, which defaults toEXCLUSIVE.INCLUSIVEis refused on create and when you switch an existing document to it; only a legacy document can still carry it. A quote with taxed lines converts only when its method isEXCLUSIVE, orINCLUSIVEon a legacy quote.CreateOrganizationTaxConfigurationInputrequiresdefaultTaxCalculationandtaxSystemType.UpdateOrganizationInputcovers the organization's name, address,country,currency,dateFormat,language,timezone,taxId,registrationNumber,industry,size,subdomain,requireCustomerApproval,creditLimitEnabled, and more.requireCustomerApprovalparks new customers inPENDING_APPROVAL, andcreditLimitEnabledset tofalseswitches off credit-limit checks. See customers and partners.TenantSetting.valueis JSON. Settings are keyed by a category and a key, so always use the category and key thattenantSettingsByCategoryreturns.SalesPolicyhasmaxDiscountPercentandrequireInvoiceBeforeShip.updateSalesPolicytakesmaxDiscountPercent.
mutation SetStockPolicy($input: InventorySettingsInput!) {
updateInventorySettings(input: $input) {
key
value
category
dataType
}
}
{
"input": {
"allowNegativeStock": false
}
}
query TaxDefaults {
organizationTaxConfig {
defaultTaxCalculation
defaultSalesTaxGroupId
defaultPurchaseTaxGroupId
enableTaxInclusive
homeCurrency
}
}
6. Reference data you cache
Warehouses, tax rates and groups, units of measure, and currencies are the reference data an integration caches. Reading warehouses, tax, units of measure and currencies needs only a signed-in staff user. Reading exchange rates needs a currency permission, as listed below. An API-key integration reads warehouses, units of measure and payment terms from the Service API read endpoints instead.
Reads:
warehouses(filter: WarehouseFilterInput)andwarehouse(id: ID!). Authenticated only.defaultWarehouse(partnerId: ID)returns the partner's default warehouse. If the partner has none, it uses the organization's setting, then the firstACTIVEwarehouse. It returns an error when no warehouse is configured.taxRates(filters: TaxRateFiltersInput, skip: Int, take: Int)andtaxGroups(filters, skip, take). Authenticated only.taxRatesByLocation(country: String!, state, city, postalCode),taxRate(id)andtaxGroup(id). Authenticated only.getUOMs(filters: UOMFiltersInput, skip: Int, take: Int)andgetUOM(id). Authenticated only.currenciesreturns the currencies configured for your organization. Authenticated only.currency(id: String!)andexchangeRates(baseCurrency: String). Requiresettings:currency:readorsettings:tenant:read.tenantExchangeRatesreturnsTenantExchangeRateSettings!. Requires any ofsettings:currency:read,settings:currency:updateorsettings:tenant:read.
Writes:
createWarehouse(input: CreateWarehouseInput!),updateWarehouseandremoveWarehouse. Role-gated:ADMINorMANAGER.createTaxRate,createTaxGroup,updateTaxRate,updateTaxGroup,deleteTaxRate,deleteTaxGroupandbulkImportTaxGroups. Role-gated:ADMINorMANAGER.createUOM(data: CreateUOMInput!),updateUOManddeleteUOM. Role-gated:ADMINorMANAGER.setBaseCurrency(currencyId: String!): Currency!. Role-gated:ADMINorMANAGER.createTenantExchangeRate(input: CreateTenantExchangeRateInput!)andendTenantExchangeRate. Requiresettings:currency:updateorsettings:tenant:update.
The rules for reference data:
- Warehouses.
codemust be unique in your organization. A duplicate is refused with the messageWarehouse code "…" is already in use.A warehouse that still has stock on hand cannot be deleted, and the refusal endsTransfer the stock to another warehouse or adjust it to 0 first.WarehouseStatusvalues areACTIVE,INACTIVEandMAINTENANCE.CreateWarehouseInputrequirescode,nameandtype. Purchasing and inventory use warehouses too; see purchasing and inventory. - Tax.
TaxRate.rateis a number. Documents should reference the tax group id, because the pricing engine resolves tax. Do not recompute tax from a cached rate. - Currencies.
Currency.codeis the ISO 4217 code, andisBaseCurrencymarks the base currency. Never convert prices on the client. A tenant exchange rate is returned as an exact decimal string inrate. One unit of the source currency equalsrateunits of the base currency. A rate applies from a calendar day on the organization's clock and never starts before today. Itsstatusis one ofIN_FORCE,SUPERSEDED,SCHEDULED,ENDEDorWITHDRAWN. - Units of measure.
UnitOfMeasurehascode,name,symbol,category,isBase,decimalPlacesandallowFractional.categoryis one ofAREA,COUNT,CUSTOM,LENGTH,TEMPERATURE,TIME,VOLUMEorWEIGHT. RespectdecimalPlacesandallowFractionalwhen you send quantities. See catalog and pricing for how UOM ids are used on products. - Paging.
taxRates,taxGroupsandgetUOMsuseskipandtake.warehousesandcurrenciesreturn the full list. For the shapes, see errors, pagination, and idempotency.
query ReferenceData {
warehouses(filter: { status: [ACTIVE] }) { id code name type status }
taxRates(take: 100, skip: 0) { id code name rate country isActive isDefault groupId }
getUOMs(take: 100, skip: 0) { id code name symbol category isBase decimalPlaces allowFractional }
currencies { id code name symbol decimalPlaces isBaseCurrency isActive }
}
To create a warehouse, send the required fields and read back its id and status:
mutation CreateWarehouse {
createWarehouse(input: {code: "<code>", name: "<name>", type: "<type>"}) {
id
code
status
}
}
Keep a copy of this data for each tenant, keyed by the ARC id. Refresh it on a schedule, and re-read an item when an id stops resolving.
7. Reading the audit trail
The activity log is a read-only, organization-wide record of business-data changes and authentication events. Use it to answer "who changed this setting, role or key". It is separate from tenantSettingHistory, which reads the history of one setting.
activityLogs(filter: ActivityLogFilterInput, page: Int = 1, pageSize: Int = 25),activityLog(id: String!),activityLogFacetsandactivityLogAnalytics(filter). All four requireaudit:activity_log:read.
The rules for the audit trail:
- The response is page-based.
pageSizeis clamped to 1 to 100 and defaults to 25. The response carriesitems,page,pageSizeandtotal. ActivityLogFilterInputtakesaction,actorId,dateFrom,dateTo,entityType,searchandsource.sourceisAUDITfor business-data changes orAUTHfor authentication events.- The log covers your organization only. You cannot choose another organization.
- Courier-cost metadata is removed from entries unless the viewer holds
sales:sales_order:ship. - Secrets never appear in the trail. An API key's value is not recorded.
- Use
activityLogFacetsto find the validactionandentityTypevalues before you filter. - The log is for reconciling changes, not an event feed. For push notifications, use webhooks; for the other event surfaces, see events and webhooks.
query Audit($filter: ActivityLogFilterInput, $page: Int, $pageSize: Int) {
activityLogs(filter: $filter, page: $page, pageSize: $pageSize) {
items {
id
createdAt
action
entityType
entityId
entityLabel
summary
actorId
actorEmail
source
changes
}
page
pageSize
total
}
}
{
"filter": {
"entityType": "ApiKey",
"dateFrom": "<ISO-8601 timestamp>"
},
"page": 1,
"pageSize": 50
}
ApiKey is the entityType that records key creation and revocation.
Integration rules
- Never use an organization id to choose a tenant. The tenant always comes from your credential. Where an operation takes an
idfor your organization (organization,updateOrganization), pass theorganizationIdfromme. To list users, useusersFiltered,userandme. - Do not assume that the tenant
ADMINrole or an Owner label grants an operation. Checkme.permissions. The role-gated operations on this page checkme.roleinstead, so for those, holding a permission alone does not unlock them. - Do not cache permissions or roles for long. Re-read
me.permissionsafter aFORBIDDENerror, and do not treat one successful call as proof that a later call is allowed. - Do not try to set another user's password. There is no such field. Invite the user or send a reset link. Reset links are rate-limited and only go to
ACTIVEusers. - Never log
ApiKeyResult.keyor store it anywhere but a secret manager. Put it there when you create it. Revocation is permanent and immediate. Rotate by creating a new key, deploying it, and then revoking the old id. Creating and revoking keys needs a JWT, not an API key. - Do not create roles or grant permissions you do not hold. Escalation is refused, and the Owner and system roles cannot be changed.
- Do not change base currency after go-live. Do not convert prices on the client.
- Do not recompute tax or UOM rounding from cached reference data. Use the tax group id,
decimalPlacesandallowFractional, and let the server calculate. - A warehouse with stock on hand cannot be deleted. Transfer or adjust the stock first.
- Treat reference reads as plain lists. Some are unbounded and some use
skipandtake. Keep a checkpoint per tenant and refresh on a schedule. - Use
@example.comaddresses in examples and tests. Invite and reset emails go out for real in environments where an email provider is configured.
Next: Analytics and search.