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_ONLYkey is enough for steps 1 to 4, 7 and 8. Creating a quote or an order needs aREAD_WRITEkey. - 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:readandsales:sales_order:readcover 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'ssalePriceand itspricesrows from step 4 are the stored prices, as the seller entered them; thecalculateVariantPriceGraphQL 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 send0expecting 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, andtotalAmountserver-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-Keyheader. 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 / message | Meaning | Action |
|---|---|---|
401 | Missing, malformed, expired, revoked, or IP-restricted key | Check the secret, key status, host, and allowlist. Do not retry forever. |
401 with READ_ONLY scope in the message | A write route was called with a read-only key | Create a replacement with READ_WRITE; do not elevate a shared key casually. |
402 | The workspace reached its plan's monthly quote limit (quote route) | Do not retry. Ask the organization administrator. |
403 / plan access error | The tenant is not entitled to API access | Ask the organization administrator or Growmax. |
400 on a read | An id that is not a UUID, a limit outside 1 to 100, or an unknown status | Fix the request. See query parameters. |
404 on a read | The id is not in the key's workspace, or the record was removed | Re-read the list; do not reuse ids from another workspace. |
429 with code API_KEY_RATE_LIMITED | The key used up its requests for this minute | Wait Retry-After seconds, then retry. Spread a large sync over time. |
| Idempotency conflict | Same key was reused with a different request body | Generate a new key for a new logical operation; inspect the original result first. |
| Validation or business error | The ids, document state, stock, credit, or required fields are invalid | Correct the input or refresh the document state; do not blindly retry. |
Next: Service API and errors, pagination, and idempotency.