Skip to main content

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!. Requires users:user:create.
  • usersFiltered(input: GetUsersInput). Authenticated only.
  • user(id: String!). Authenticated only.
  • updateUser(id: String!, input: UpdateUserInput!). Requires users:user:update.
  • deleteUser(id: String!). Requires users:user:delete.
  • sendPasswordResetLink(userId: ID!). Requires users:user:update.
  • userRoles(userId: String!). Requires users: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.email is 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 message User 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. CreateUserInput still lists an organizationId field, but ARC discards it. Leave it out.
  • CreateUserInput has no password field. A user created with status ACTIVE gets an invite email with a link valid for 24 hours. A user created as INACTIVE or SUSPENDED gets no email. After you set them to ACTIVE, send the link with sendPasswordResetLink.
  • sendPasswordResetLink sends 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 only ACTIVE users can be sent a link.
  • Naming a role on create or update, through roleId or the role enum, also requires settings: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. Prefer roleId: a role enum 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.
  • UserStatus values are ACTIVE, INACTIVE and SUSPENDED. The staff values of the UserRole enum are ADMIN, MANAGER, OWNER, SALES_REP, VIEWER and WAREHOUSE_STAFF. The enum also lists buyer and platform values that staff provisioning does not use. Staff operations refuse customer sign-ins.
  • usersFiltered is page-based. Send pagination with page and limit, and read totalCount and hasNextPage. 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) and role(id: ID!). Authenticated only.
  • createRole(input: CreateRoleInput!). Requires settings:role:create.
  • cloneRole(input: CloneRoleInput!). Requires settings:role:create.
  • updateRole(input: UpdateRoleInput!). Requires settings:role:update.
  • restoreRole(id). Requires settings:role:update.
  • removeRole(id). Requires settings:role:delete. Returns Boolean!.
  • assignUserToRole(input: AssignUserToRoleInput!). Requires settings:role:update. Returns Boolean!.
  • removeUserFromRole. Requires settings:role:update.
  • assignPermissionToRole(input: AssignPermissionInput!). Requires settings:permission:update. Returns Boolean!.
  • removePermissionFromRole. Requires settings:permission:update.
  • updateRolePermissions(roleId: ID!, permissionIds: [ID!]!). Requires settings:permission:update. Returns Boolean!.
  • assignUserPermission(input: AssignUserPermissionInput!) and removeUserPermission. Require settings:permission:update.

The inputs:

  • CreateRoleInput: name and displayName are required. description, isActive, metadata and priority are optional.
  • CloneRoleInput: sourceRoleId, newRoleName and newRoleDisplayName are required. description is optional, includePermissions defaults to true, and includeUsers defaults to false.
  • AssignUserToRoleInput: userId and roleId are required. expiresAt is optional.
  • AssignPermissionInput: roleId and permissionId are required. conditions is optional.
  • AssignUserPermissionInput: userId and permissionId are required. granted defaults to true. expiresAt and conditions are 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 message The Owner role's permissions cannot be modified.
  • System roles cannot be modified or deleted, and the refusal reads System roles cannot be modified or System 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 restoreRole instead 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: ADMIN or MANAGER. If API access is not enabled on your organization's plan, the call is refused. Ask your Growmax account manager to enable it.
  • apiKeys and apiKey(id: String!). Require settings:api:read or settings:tenant:read.
  • revokeApiKey(id: String!): Boolean!. Role-gated: ADMIN or MANAGER. Also refused when API access is not enabled on the plan.

The rules for keys:

  • CreateApiKeyInputGql requires name. scope is READ_ONLY by default, or READ_WRITE. expiresAt must be in the future. expiresInDays takes 1 to 3650, and expiresAt wins when you send both. allowedIps takes IPv4 addresses, IPv6 addresses or CIDR ranges, and an empty list allows any address. rateLimit is 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 answers 429 with code API_KEY_RATE_LIMITED when it is used up. It cannot be changed after creation; create a replacement key instead. See rate limits.
  • ApiKeyResult.key is returned only in the createApiKey response. maskedKey, keyPrefix, lastUsedAt, usageCount, revokedAt and isActive are 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.role must be ADMIN or MANAGER.
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 with Unauthorized to view this organization.
  • updateOrganization(id: String!, input: UpdateOrganizationInput!): Organization!. Requires settings:organization:update.
  • updateInventorySettings(input: InventorySettingsInput!). Requires settings:inventory:update or settings:tenant:update.
  • tenantSettingsByCategory(category: String!) and tenantSettingHistory. Require settings:tenant:read.
  • updateTenantSetting(input: UpdateTenantSettingInput!) and bulkUpdateTenantSettings. Require settings:tenant:update.
  • salesPolicy reads the sales policy. It requires any of settings:tenant:read, sales:quote:create, sales:quote:update, sales:sales_order:create, sales:sales_order:update or sales:sales_order:ship.
  • updateSalesPolicy(input: UpdateSalesPolicyInput!) changes it. Requires settings:sales:update or settings:tenant:update.
  • organizationTaxConfig. Authenticated only.
  • createOrUpdateOrganizationTaxConfig(input: CreateOrganizationTaxConfigurationInput!). Role-gated: ADMIN or MANAGER.

The rules for settings:

  • Allow Negative Stock is the allow_negative_stock key in the inventory settings category. updateInventorySettings takes allowNegativeStock and writes it to that key. When the setting is off, confirming a sales order (from DRAFT or AWAITING_PAYMENT to CONFIRMED) 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 change defaultWarehouse, the defaultWarehouse query returns the new value.
  • Base currency is changed through updateOrganization (its currency field) or through setBaseCurrency, 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 reads Cannot 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.
  • defaultTaxCalculation holds the tenant's default tax calculation method. Documents carry their own taxCalculationMethod. Quote conversion refuses a taxed line that has no valid method, and it never falls back to EXCLUSIVE. The enum lists ADDITIVE, COMPOUND, EXCLUSIVE and INCLUSIVE, but new quotes, sales orders and invoices are EXCLUSIVE-only: send EXCLUSIVE or omit the method, which defaults to EXCLUSIVE. INCLUSIVE is 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 is EXCLUSIVE, or INCLUSIVE on a legacy quote. CreateOrganizationTaxConfigurationInput requires defaultTaxCalculation and taxSystemType.
  • UpdateOrganizationInput covers the organization's name, address, country, currency, dateFormat, language, timezone, taxId, registrationNumber, industry, size, subdomain, requireCustomerApproval, creditLimitEnabled, and more. requireCustomerApproval parks new customers in PENDING_APPROVAL, and creditLimitEnabled set to false switches off credit-limit checks. See customers and partners.
  • TenantSetting.value is JSON. Settings are keyed by a category and a key, so always use the category and key that tenantSettingsByCategory returns.
  • SalesPolicy has maxDiscountPercent and requireInvoiceBeforeShip. updateSalesPolicy takes maxDiscountPercent.
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) and warehouse(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 first ACTIVE warehouse. It returns an error when no warehouse is configured.
  • taxRates(filters: TaxRateFiltersInput, skip: Int, take: Int) and taxGroups(filters, skip, take). Authenticated only.
  • taxRatesByLocation(country: String!, state, city, postalCode), taxRate(id) and taxGroup(id). Authenticated only.
  • getUOMs(filters: UOMFiltersInput, skip: Int, take: Int) and getUOM(id). Authenticated only.
  • currencies returns the currencies configured for your organization. Authenticated only.
  • currency(id: String!) and exchangeRates(baseCurrency: String). Require settings:currency:read or settings:tenant:read.
  • tenantExchangeRates returns TenantExchangeRateSettings!. Requires any of settings:currency:read, settings:currency:update or settings:tenant:read.

Writes:

  • createWarehouse(input: CreateWarehouseInput!), updateWarehouse and removeWarehouse. Role-gated: ADMIN or MANAGER.
  • createTaxRate, createTaxGroup, updateTaxRate, updateTaxGroup, deleteTaxRate, deleteTaxGroup and bulkImportTaxGroups. Role-gated: ADMIN or MANAGER.
  • createUOM(data: CreateUOMInput!), updateUOM and deleteUOM. Role-gated: ADMIN or MANAGER.
  • setBaseCurrency(currencyId: String!): Currency!. Role-gated: ADMIN or MANAGER.
  • createTenantExchangeRate(input: CreateTenantExchangeRateInput!) and endTenantExchangeRate. Require settings:currency:update or settings:tenant:update.

The rules for reference data:

  • Warehouses. code must be unique in your organization. A duplicate is refused with the message Warehouse code "…" is already in use. A warehouse that still has stock on hand cannot be deleted, and the refusal ends Transfer the stock to another warehouse or adjust it to 0 first. WarehouseStatus values are ACTIVE, INACTIVE and MAINTENANCE. CreateWarehouseInput requires code, name and type. Purchasing and inventory use warehouses too; see purchasing and inventory.
  • Tax. TaxRate.rate is 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.code is the ISO 4217 code, and isBaseCurrency marks the base currency. Never convert prices on the client. A tenant exchange rate is returned as an exact decimal string in rate. One unit of the source currency equals rate units of the base currency. A rate applies from a calendar day on the organization's clock and never starts before today. Its status is one of IN_FORCE, SUPERSEDED, SCHEDULED, ENDED or WITHDRAWN.
  • Units of measure. UnitOfMeasure has code, name, symbol, category, isBase, decimalPlaces and allowFractional. category is one of AREA, COUNT, CUSTOM, LENGTH, TEMPERATURE, TIME, VOLUME or WEIGHT. Respect decimalPlaces and allowFractional when you send quantities. See catalog and pricing for how UOM ids are used on products.
  • Paging. taxRates, taxGroups and getUOMs use skip and take. warehouses and currencies return 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!), activityLogFacets and activityLogAnalytics(filter). All four require audit:activity_log:read.

The rules for the audit trail:

  • The response is page-based. pageSize is clamped to 1 to 100 and defaults to 25. The response carries items, page, pageSize and total.
  • ActivityLogFilterInput takes action, actorId, dateFrom, dateTo, entityType, search and source. source is AUDIT for business-data changes or AUTH for 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 activityLogFacets to find the valid action and entityType values 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 id for your organization (organization, updateOrganization), pass the organizationId from me. To list users, use usersFiltered, user and me.
  • Do not assume that the tenant ADMIN role or an Owner label grants an operation. Check me.permissions. The role-gated operations on this page check me.role instead, so for those, holding a permission alone does not unlock them.
  • Do not cache permissions or roles for long. Re-read me.permissions after a FORBIDDEN error, 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 ACTIVE users.
  • Never log ApiKeyResult.key or 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, decimalPlaces and allowFractional, 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 skip and take. Keep a checkpoint per tenant and refresh on a schedule.
  • Use @example.com addresses in examples and tests. Invite and reset emails go out for real in environments where an email provider is configured.

Next: Analytics and search.