In short
Authorization: Bearer <key> to one base URL. The key's prefix, not the URL, decides live or test.data, pagination and order; a single object returns data. Never a bare array.code, never on message, and keep the request_id for support.The NutraSoft API is a REST API over your ERP data: the catalog, lots and warehouses, production runs, sales and purchasing. It speaks JSON, it is authenticated with an API key, and every response has the same envelope. Three steps get you from nothing to an order in your test account.
In Settings, Developer, create your test account on the Sandbox card, then a test key. It is shown once, so store it before you close the dialog.
Put the key in NUTRASOFT_API_KEY and run this. It needs no scope. Expect environment to be test.
curl "https://public-api.nutrasoft.ca/v1/organization" \
-H "Authorization: Bearer $NUTRASOFT_API_KEY"{
"data": {
"api_key_id": 123,
"company": "Example Foods Inc.",
"currency": "CAD",
"environment": "test",
"key_prefix": "nsk_test_9fA",
"license_id": 123,
"rate_limit_per_min": 300,
"scopes": [
"catalog:read",
"sales:read",
"sales:write"
]
}
}Take a customer id from GET /customers and a product id from GET /products, then post the order. It stays in your test account.
curl -X POST "https://public-api.nutrasoft.ca/v1/sales-orders" \
-H "Authorization: Bearer $NUTRASOFT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_id": 123,
"lines": [
{
"order_qty": 12.5,
"product_id": 123
}
]
}'{
"data": {
"id": 123,
"billing_address": {
"city": "Montreal",
"line1": "123 Example Street",
"line2": "Suite 200",
"postal_code": "H4S 2C1",
"state": "QC"
},
"company": "Example Foods Inc.",
"created_at": "2026-09-10T14:30:00Z",
"customer_id": 123,
"customer_purchase_order_no": "PO-10042",
"expected_date": "2026-09-10",
"lines": [
{
"id": 123,
"cancelled_qty_base": 12.5,
"discount_rate": 12.5,
"notes": "string",
"order_qty": 12.5,
"order_qty_base": 12.5,
"price": 12.5,
"product_code": "SAMPLE-01",
"product_id": 123,
"product_name": "Sample name",
"shipped_qty_base": 12.5,
"tax1": false,
"tax2": false,
"uom_type": "INVENTORY_UNIT"
}
],
"order_date": "2026-09-10",
"sales_order_no": 10,
"ship_date": "2026-09-10",
"shipping_address": {
"city": "Montreal",
"line1": "123 Example Street",
"line2": "Suite 200",
"postal_code": "H4S 2C1",
"state": "QC"
},
"status": "OPEN",
"subtotal": 12.5,
"tax1": false,
"tax1_rate": 12.5,
"tax2": false,
"tax2_rate": 12.5,
"tracking_number": "string",
"updated_at": "2026-09-10T14:30:00Z"
}
}| Prefix | Environment | Reads and writes |
|---|---|---|
nsk_live_ | Live | Your real business data |
nsk_test_ | Test | Your test account, a separate copy you can safely break |
There is one base URL for both environments. The key decides which one you are in, so a test suite cannot be pointed at live data by editing a URL. Check environment in your test setup: it is the cheapest guard against a suite that has quietly been pointed at production. See Test mode.
A list comes back as data, pagination and order. A single object comes back as {"data": {...}}. Nothing is ever a bare array, so fields can be added without breaking you.
{
"data": [{ "id": 123 }],
"pagination": { "limit": 50, "offset": 0, "total": 412, "has_more": true },
"order": "name:asc,id:asc"
}Use has_more to decide whether to fetch the next page. Conventions covers paging, sorting and null values.
Every error has the same shape. Branch on code, never on message: codes are added over time but never renamed or repurposed, while messages are written for people and may change.
{
"error": {
"code": "INVALID_API_KEY",
"message": "The API key is missing, malformed, revoked or not valid for this environment.",
"details": null,
"request_id": "req_4f1c8a90b7e24d3f9c0a5b6e7d8f9012"
}
}| What you see | What to check |
|---|---|
| 401 UNAUTHENTICATED | No Authorization header, or it is not Bearer <key>. |
| 401 INVALID_API_KEY | A wrong or revoked key, or one whose nsk_live_ or nsk_test_ prefix does not match its account. |
| 403 NOT_ENTITLED | The account is not on a paid Enterprise plan, or its plan has lapsed. |
| 403 INSUFFICIENT_SCOPE | The key does not carry the scope the operation needs. |
| 429 RATE_LIMITED | Slow down. Retry-After says how many seconds to wait. |
Keep the request_id. It is in every error body and in the X-Request-Id header of every response, and it is what lets support find your exact request.