Skip to main content

Make your first service API request

ARC's stable machine-to-machine REST surface is intentionally small. It reads the catalog, customers, reference lists and document status, creates sales quotes and sales orders, and runs bulk product imports. It is not a second copy of the entire GraphQL schema.

This walkthrough starts with reads, so a read-only key is enough to begin. Every id the writes need comes from those reads; you do not need to copy ids out of the ARC console.

Prerequisites​

Ask an organization administrator for:

  • the tenant's ARC API host;
  • a tenant API key created in Settings → API keys. A READ_ONLY key is enough for steps 1 to 4, 7 and 8. Creating a quote or an order needs a READ_WRITE key.
  • The key acts as the user who created it, so that user needs read permission on what you read: products:product:read, customers:customer:read, inventory:stock_level:read and sales:sales_order:read cover this walkthrough. Ask for the key to be created by a dedicated integration user. The full list is in the Service API reference.
export ARC_API_URL="https://<arc-host>"
export ARC_API_KEY="gm_live_<48-hex-characters>"

1. Check the host​

Health is unauthenticated and is useful for checking DNS, TLS, and deployment availability. It does not prove that your API key or tenant access works.

curl --fail-with-body "$ARC_API_URL/health"

2. Make your first read: payment terms​

This call proves that the key, the host and the workspace's API access all work. It returns the payment terms an order may use:

curl --fail-with-body --include \
--header "Authorization: Bearer $ARC_API_KEY" \
"$ARC_API_URL/api/service/payment-terms"
{
"success": true,
"data": [
{
"code": "<payment-term-code>",
"label": "<payment-term-label>",
"kind": "NET",
"netDays": 30,
"isDefault": true,
"isActive": true
}
]
}

Keep a code for the order in step 6. --include prints the response headers too: X-ApiKey-RateLimit-Limit, X-ApiKey-RateLimit-Remaining and X-ApiKey-RateLimit-Reset show the key's per-minute budget. See Rate limits and request limits.

3. Find the customer​

Search by name, code, or a contact's value, such as an email address or phone number:

curl --fail-with-body --get "$ARC_API_URL/api/service/customers" \
--header "Authorization: Bearer $ARC_API_KEY" \
--data-urlencode "search=<customer-name-or-code>"

From the matching row, keep id as the customerId. The row also carries the customer's own paymentTerms, currency, addresses and contacts. The Service API guide shows a full customer row.

4. Find the product, variant and unit​

Search the catalog by product name, SKU, barcode or brand:

curl --fail-with-body --get "$ARC_API_URL/api/service/products" \
--header "Authorization: Bearer $ARC_API_KEY" \
--data-urlencode "search=<sku>" \
--data-urlencode "limit=5"

Keep the product's id and the id of the variant you want from variants. Then read the product itself for its unit rows and price ladder:

curl --fail-with-body \
--header "Authorization: Bearer $ARC_API_KEY" \
"$ARC_API_URL/api/service/products/<product-id>"

An excerpt of the response:

{
"success": true,
"data": {
"id": "<product-id>",
"name": "<product-name>",
"salesUom": { "id": "<uom-id>", "code": "<uom-code>", "name": "<uom-name>", "symbol": "<uom-symbol>" },
"variants": [
{
"id": "<variant-id>",
"sku": "<sku>",
"salePrice": 120,
"prices": [
{ "minQuantity": 10, "price": 110, "currency": "<currency-code>", "uomId": "<uom-id>" }
]
}
],
"uoms": [
{ "uomId": "<uom-id>", "variantId": null, "isBase": true, "isSales": true, "conversionFactor": 1 }
]
}
}

Take uomId from a uoms row with isSales or isBase set, for your variant or with no variantId. If uoms is empty, use the product's salesUom or baseUom.

To see what is available before you order, read stock for one or more variants:

curl --fail-with-body \
--header "Authorization: Bearer $ARC_API_KEY" \
"$ARC_API_URL/api/service/stock?variantIds=<variant-id>"

data has one row per variant and active warehouse. Any id that is not a variant in your workspace is returned in unknownVariantIds instead of data.

5. Create a quote​

This step and the next need a READ_WRITE key. The service route returns { "success": true, "data": ... } on success, where data has the same fields as GET /api/service/sales-quotes/:id (or sales-orders/:id for an order).

curl --fail-with-body --request POST "$ARC_API_URL/api/service/sales-quotes" \
--header "Authorization: Bearer $ARC_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: quote-sync-2026-09-18-0001" \
--data @quote.json

quote.json is an input shape, not a tenant-independent fixture. Fill every placeholder from the reads above:

{
"customerId": "<customer-id>",
"currency": "<currency-code>",
"items": [
{
"productId": "<product-id>",
"variantId": "<variant-id>",
"productName": "<product-name>",
"uomId": "<uom-id>",
"quantity": 10,
"unitPrice": <resolved-unit-price>,
"discount": 0,
"discountType": "PERCENTAGE",
"taxAmount": 0,
"taxRate": 0
}
],
"discountAmount": 0,
"discountType": "PERCENTAGE",
"discountValue": 0,
"shippingCost": 0,
"subtotal": 0,
"taxAmount": 0,
"taxRate": 0,
"totalAmount": 0,
"idempotencyKey": "quote-sync-2026-09-18-0001"
}

Money rules that matter on this route:

  • Quote lines require unitPrice. Resolve it before creating the quote. The variant's salePrice and its prices rows from step 4 are the stored prices, as the seller entered them; the calculateVariantPrice GraphQL query, which needs a staff user, returns the pricing engine's answer. Do not ship a hardcoded catalog price from an old sync, and do not send 0 expecting the server to fill it in: on quotes the line price you send is the commercial input.
  • Header totals are advisory. ARC recomputes subtotal, taxAmount, and totalAmount server-side from the accepted lines; send your best calculation and read the persisted values back from the response.
  • The same idempotency key may be supplied in the body or the Idempotency-Key header. Use one stable key for a retry of the same logical create request. Reusing a key with a different payload is a conflict, not a new quote.

6. Create a sales order​

The order route follows the same authentication and response envelope:

curl --fail-with-body --request POST "$ARC_API_URL/api/service/sales-orders" \
--header "Authorization: Bearer $ARC_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: order-sync-2026-09-18-0001" \
--data @sales-order.json
{
"customerId": "<customer-id>",
"paymentTerms": "<payment-term-code>",
"items": [
{
"productId": "<product-id>",
"variantId": "<variant-id>",
"quantity": 10,
"uomId": "<uom-id>"
}
],
"idempotencyKey": "order-sync-2026-09-18-0001"
}

paymentTerms must be a code from step 2, and uomId is required on every order line. Without currency, the order is in the workspace's base currency. Without unitPrice, the server resolves each line's price.

Orders are created through the canonical order service. That service owns numbering, pricing, inventory behavior, credit checks, and audit attribution. Do not send organizationId or attempt to select the acting user in the body; the tenant comes from the key and the audit actor is the user who created it.

7. Read the order back​

Use the id from the create response to follow the order's status:

curl --fail-with-body \
--header "Authorization: Bearer $ARC_API_KEY" \
"$ARC_API_URL/api/service/sales-orders/<sales-order-id>"

The response carries status, paymentStatus, the totals with their currency, and each line's shippedQuantity and invoicedQuantity. To follow many documents, page GET /api/service/sales-orders?updatedAfter=<ISO-8601> instead. See Delta sync.

8. Read bulk-import status​

Product import status and preview are read-only service operations and can be called with a read-only key:

curl --fail-with-body \
--header "Authorization: Bearer $ARC_API_KEY" \
"$ARC_API_URL/api/service/products/bulk/<import-id>"

curl --fail-with-body \
--header "Authorization: Bearer $ARC_API_KEY" \
"$ARC_API_URL/api/service/products/bulk/<import-id>/preview"

The import write lifecycle is:

POST /api/service/products/bulk → returns importId and uploadUrl
→ PUT the file bytes to uploadUrl
→ POST /:importId/stage → queues parsing
→ GET /:importId → poll until status is PREVIEW
→ GET /:importId/preview
→ POST /:importId/approve → requires PREVIEW
→ GET /:importId → poll until COMPLETED or FAILED

Create, stage, approve, and rollback require READ_WRITE. See Bulk product imports for the upload step and the full status list. Treat the import id as tenant-scoped and never reuse one from another connection.

Common failures​

Status / messageMeaningAction
401Missing, malformed, expired, revoked, or IP-restricted keyCheck the secret, key status, host, and allowlist. Do not retry forever.
401 with READ_ONLY scope in the messageA write route was called with a read-only keyCreate a replacement with READ_WRITE; do not elevate a shared key casually.
402The workspace reached its plan's monthly quote limit (quote route)Do not retry. Ask the organization administrator.
403 / plan access errorThe tenant is not entitled to API accessAsk the organization administrator or Growmax.
400 on a readAn id that is not a UUID, a limit outside 1 to 100, or an unknown statusFix the request. See query parameters.
404 on a readThe id is not in the key's workspace, or the record was removedRe-read the list; do not reuse ids from another workspace.
429 with code API_KEY_RATE_LIMITEDThe key used up its requests for this minuteWait Retry-After seconds, then retry. Spread a large sync over time.
Idempotency conflictSame key was reused with a different request bodyGenerate a new key for a new logical operation; inspect the original result first.
Validation or business errorThe ids, document state, stock, credit, or required fields are invalidCorrect the input or refresh the document state; do not blindly retry.

Next: Service API and errors, pagination, and idempotency.