Hundred API reference
Hundred is a JSON REST API in front of Sage 100 (formerly MAS 90 / MAS 200), Standard, Advanced and Premium. Your application calls one HTTPS endpoint. A small connector on the customer's Sage 100 server carries each call into Sage 100 and returns the result.
| Base URL | https://api.hundredapi.com/v1 |
| Format | JSON in and out, UTF-8. Dates as YYYY-MM-DD, timestamps as ISO 8601 UTC. Money as decimal numbers in the company's currency. |
| Names | Where a Sage 100 field exists, the API uses its name in camelCase, so CustomerNo becomes customerNo and QuantityOnHand becomes quantityOnHand. Computed fields (id, orderTotal) are marked in the reference. |
| Versions | Sage 100 versions under Sage support at launch. Sage 100 2026 is 64-bit only; the connector ships 64-bit. |
| SDKs | Python and TypeScript, generated from the OpenAPI document. A free read-only Python package for direct ODBC access comes first. |
Authentication
Every request carries a secret key as a bearer token. Keys are per environment: hk_test_… reaches only sandbox companies, hk_live_… reaches production companies. Keep keys on your server; never put them in a browser or mobile app.
curl https://api.hundredapi.com/v1/companies \ -H "Authorization: Bearer $HUNDRED_KEY"
A missing or wrong key returns 401. A valid key for a company your account is not connected to returns 404, so a key never tells you which other companies exist.
Companies
A Sage 100 installation holds several companies, each with a three-character company code. Every resource lives under one company:
GET /v1/companies
GET /v1/companies/{companyCode}/customers
{
"data": [
{ "companyCode": "ABC", "name": "ABC Distribution and Service Corp.",
"connector": { "status": "online", "sageVersion": "2026.1", "lastSeenAt": "2026-09-27T08:14:02Z" } }
]
}
Billing is per connected company. A company is connected when the customer authorizes it in the connector.
Pagination and filters
List endpoints use cursor pagination. Pass limit (default 100, maximum 500) and, for the next page, the nextCursor of the previous response. nextCursor is null on the last page. Cursors are opaque; do not build them yourself.
GET /v1/companies/ABC/customers?limit=2&updatedSince=2026-09-01T00:00:00Z
{
"data": [ { "id": "01-ABF", "…": "…" }, { "id": "01-AVNET", "…": "…" } ],
"nextCursor": "Y3VzdDowMS1BVk5FVA"
}
| Parameter | Meaning |
|---|---|
limit | Page size, 1 to 500. |
cursor | The nextCursor from the previous page. |
updatedSince | Only records changed at or after this time. Use it for incremental sync instead of re-reading everything. |
fields | Comma-separated field list, to keep large reads fast. |
Resource-specific filters (for example status on sales orders) are listed with each resource. Results come in key order (the Sage 100 primary key), which is the fastest order for the Sage 100 data engine.
Writes and idempotency
Every write goes through Sage 100's own business objects, the same logic the desktop client uses. Numbering, defaults, credit checks, tax and validation rules behave exactly as they do for a user at the keyboard. Hundred never writes to the data files or SQL tables directly.
Send an Idempotency-Key header on every POST. If the network drops after Sage 100 saved the record, repeating the call with the same key returns the first result instead of creating a second order.
curl -X POST https://api.hundredapi.com/v1/companies/ABC/sales-orders \
-H "Authorization: Bearer $HUNDRED_KEY" \
-H "Idempotency-Key: shopify-order-5512" \
-H "Content-Type: application/json" \
-d '{ "customerId": "01-AVNET", "customerPoNo": "5512",
"lines": [ { "itemCode": "1001-HON-H252", "quantityOrdered": 4 } ] }'
Errors
Errors return a JSON body with a stable type, a readable message and, when Sage 100 refused the operation, its own message unchanged in sageMessage.
{
"error": {
"type": "sage_validation",
"message": "Sage 100 rejected the sales order.",
"sageMessage": "Customer is on credit hold."
}
}
| Status | Type | When |
|---|---|---|
| 400 | invalid_request | Malformed JSON, unknown field, bad parameter. |
| 401 | unauthorized | Missing or wrong key. |
| 404 | not_found | No such record, or no such company for this key. |
| 409 | record_locked | A Sage 100 user has the record open. Retry later. |
| 422 | sage_validation | Sage 100 business logic refused the write. See sageMessage. |
| 429 | rate_limited | Too many calls for this company. Honor Retry-After. |
| 503 | connector_offline | The customer's Sage 100 server is off or has no network. |
| 504 | connector_timeout | Sage 100 did not answer in time. Safe to retry with the same idempotency key. |
Connector architecture
Sage 100 is a Windows application with no web API of its own; the old sData REST interface was removed in version 2025. So a small piece of software must run next to it. The Hundred connector is a Windows service on the Sage 100 server (or a workstation with the Sage 100 client installed).
- Outbound only. The connector opens an outbound HTTPS connection to the relay and keeps it open. The customer opens no firewall port and needs no VPN.
- Writes use the Business Object Interface (BOI). BOI is Sage 100's COM interface for its business objects, included with Sage 100 since version 4.3. It applies full Sage validation. The connector signs in to BOI as a Sage 100 user; plan for that user to take a license seat while it is connected.
- Reads use ODBC where it is faster. Standard and Advanced read through the bundled ProvideX ODBC driver (read-only); Premium reads from SQL Server. Both are sanctioned read paths.
- Least privilege. The connector signs in to Sage 100 as a dedicated Sage user that the customer creates, with role permissions limited to the modules you use.
- The customer stays in control. They see every connected app in the connector and can pause or revoke one. Hundred stores no Sage 100 business data at rest beyond short-lived request logs.
Customers
Accounts Receivable customers. Sage 100 keys a customer by AR division plus customer number, so the API id is {ARDivisionNo}-{CustomerNo}, for example 01-AVNET. Source: AR_Customer.
| GET/customers | List. Filters: updatedSince, q (name starts with), salespersonNo. |
| GET/customers/{id} | One customer. |
| POST/customers | Create. Sage 100 defaults apply for anything you leave out. |
| PATCH/customers/{id} | Update the fields you send. |
GET /v1/companies/ABC/customers/01-AVNET
{
"id": "01-AVNET",
"arDivisionNo": "01",
"customerNo": "AVNET",
"customerName": "Avnet Industries",
"addressLine1": "2131 Seventh Street",
"city": "Irvine", "state": "CA", "zipCode": "92614",
"telephoneNo": "(949) 555-0177",
"emailAddress": "ap@avnet.example",
"termsCode": "01",
"creditLimit": 25000.00,
"currentBalance": 4812.40,
"updatedAt": "2026-09-22T15:03:10Z"
}
Sales orders
Sales Order module orders and quotes, with their lines. Source: SO_SalesOrderHeader and SO_SalesOrderDetail. Writes use the Sales Order business object, so pricing, tax and credit checks run as in Sage 100.
| GET/sales-orders | List. Filters: status (open, hold, new), customerId, orderDateFrom, orderDateTo, updatedSince. |
| GET/sales-orders/{salesOrderNo} | One order with lines. |
| POST/sales-orders | Create an order. Omit salesOrderNo to take the next Sage number. |
| PATCH/sales-orders/{salesOrderNo} | Update header fields, add or change lines. |
Request
POST /v1/companies/ABC/sales-orders
Idempotency-Key: shop-5512
{
"customerId": "01-AVNET",
"customerPoNo": "5512",
"shipVia": "UPS GROUND",
"lines": [
{ "itemCode": "1001-HON-H252",
"quantityOrdered": 4 },
{ "itemCode": "6655",
"quantityOrdered": 1,
"unitPrice": 19.95 }
]
}Response 201
{
"salesOrderNo": "0001043",
"orderType": "standard",
"status": "open",
"orderDate": "2026-09-27",
"customerId": "01-AVNET",
"customerPoNo": "5512",
"lines": [
{ "lineKey": "000001",
"itemCode": "1001-HON-H252",
"quantityOrdered": 4,
"unitPrice": 129.50,
"extensionAmt": 518.00 },
{ "lineKey": "000002", "itemCode": "6655",
"quantityOrdered": 1, "unitPrice": 19.95,
"extensionAmt": 19.95 }
],
"salesTaxAmt": 43.28,
"orderTotal": 581.23
}Invoices
Posted invoices from invoice history, plus open AR balances per invoice. Unposted invoice batches are not exposed at launch, so every invoice you read is final. Source: AR_InvoiceHistoryHeader, AR_InvoiceHistoryDetail and AR_OpenInvoice.
| GET/invoices | List. Filters: customerId, invoiceDateFrom, invoiceDateTo, open=true (balance above zero). |
| GET/invoices/{invoiceNo} | One invoice with lines and open balance. |
| POST/invoices | Create a standalone invoice in an S/O invoice data entry batch (posting stays in Sage 100). |
GET /v1/companies/ABC/invoices?customerId=01-AVNET&open=true
{
"data": [{
"invoiceNo": "0100488",
"invoiceDate": "2026-09-02",
"customerId": "01-AVNET",
"salesOrderNo": "0001019",
"invoiceTotal": 2140.00,
"balance": 2140.00,
"dueDate": "2026-10-02"
}],
"nextCursor": null
}
Items
Inventory items with price, cost and stock per warehouse. Source: CI_Item and IM_ItemWarehouse.
| GET/items | List. Filters: productLine, itemType, updatedSince. |
| GET/items/{itemCode} | One item. |
| GET/items/{itemCode}/stock | Quantity on hand, on sales order and on purchase order, per warehouse. |
| PATCH/items/{itemCode} | Update price and descriptive fields. |
GET /v1/companies/ABC/items/1001-HON-H252/stock
{
"itemCode": "1001-HON-H252",
"warehouses": [
{ "warehouseCode": "000", "quantityOnHand": 42, "quantityOnSalesOrder": 8, "quantityOnPurchaseOrder": 24 },
{ "warehouseCode": "EST", "quantityOnHand": 5, "quantityOnSalesOrder": 0, "quantityOnPurchaseOrder": 0 }
]
}
Vendors
Accounts Payable vendors. The id is {APDivisionNo}-{VendorNo}. Source: AP_Vendor.
| GET/vendors | List. Filters: updatedSince, q. |
| GET/vendors/{id} | One vendor. |
| POST/vendors | Create. |
{ "id": "01-CONTAINER", "apDivisionNo": "01", "vendorNo": "CONTAINER", "vendorName": "Container Corporation", "termsCode": "02" }
Purchase orders
Purchase Order module orders with lines. Source: PO_PurchaseOrderHeader and PO_PurchaseOrderDetail.
| GET/purchase-orders | List. Filters: status, vendorId, updatedSince. |
| GET/purchase-orders/{purchaseOrderNo} | One order with lines and received quantities. |
| POST/purchase-orders | Create. |
{
"purchaseOrderNo": "0000351", "vendorId": "01-CONTAINER", "status": "open",
"lines": [ { "itemCode": "1001-HON-H252", "quantityOrdered": 24, "quantityReceived": 0, "unitCost": 71.20 } ]
}
General ledger
Read-only at launch. Source: GL_Account and GL_DetailPosting.
| GET/gl/accounts | Chart of accounts with type and status. |
| GET/gl/entries | Posted detail lines. Filters: account, postingDateFrom, postingDateTo, sourceJournal. |
GET /v1/companies/ABC/gl/entries?account=400-00-00&postingDateFrom=2026-09-01
{
"data": [
{ "account": "400-00-00", "postingDate": "2026-09-02", "sourceJournal": "SO",
"journalRegisterNo": "000213", "comment": "Invoice 0100488", "debit": 0, "credit": 2140.00 }
],
"nextCursor": "Z2w6MDAwMjEz"
}
Building against Sage 100? Tell us which resources you need first.
Join early access