Hundred/ api for Sage 100
Planned API · early accessThis is the API we are building, published so you can check it fits your integration before it ships. Nothing on this page is callable yet. Field names and limits can still change; waitlist teams hear about every change first.

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 URLhttps://api.hundredapi.com/v1
FormatJSON in and out, UTF-8. Dates as YYYY-MM-DD, timestamps as ISO 8601 UTC. Money as decimal numbers in the company's currency.
NamesWhere 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.
VersionsSage 100 versions under Sage support at launch. Sage 100 2026 is 64-bit only; the connector ships 64-bit.
SDKsPython 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"
}
ParameterMeaning
limitPage size, 1 to 500.
cursorThe nextCursor from the previous page.
updatedSinceOnly records changed at or after this time. Use it for incremental sync instead of re-reading everything.
fieldsComma-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."
  }
}
StatusTypeWhen
400invalid_requestMalformed JSON, unknown field, bad parameter.
401unauthorizedMissing or wrong key.
404not_foundNo such record, or no such company for this key.
409record_lockedA Sage 100 user has the record open. Retry later.
422sage_validationSage 100 business logic refused the write. See sageMessage.
429rate_limitedToo many calls for this company. Honor Retry-After.
503connector_offlineThe customer's Sage 100 server is off or has no network.
504connector_timeoutSage 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).

Your appapi.hundredapi.com ConnectorSage 100 HTTPS + keyauth, routing, retries Windows serviceBOI + ODBC HTTPSoutbound only customer's Windows server
  • 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/customersList. Filters: updatedSince, q (name starts with), salespersonNo.
GET/customers/{id}One customer.
POST/customersCreate. 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-ordersList. Filters: status (open, hold, new), customerId, orderDateFrom, orderDateTo, updatedSince.
GET/sales-orders/{salesOrderNo}One order with lines.
POST/sales-ordersCreate 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/invoicesList. Filters: customerId, invoiceDateFrom, invoiceDateTo, open=true (balance above zero).
GET/invoices/{invoiceNo}One invoice with lines and open balance.
POST/invoicesCreate 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/itemsList. Filters: productLine, itemType, updatedSince.
GET/items/{itemCode}One item.
GET/items/{itemCode}/stockQuantity 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/vendorsList. Filters: updatedSince, q.
GET/vendors/{id}One vendor.
POST/vendorsCreate.
{ "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-ordersList. Filters: status, vendorId, updatedSince.
GET/purchase-orders/{purchaseOrderNo}One order with lines and received quantities.
POST/purchase-ordersCreate.
{
  "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/accountsChart of accounts with type and status.
GET/gl/entriesPosted 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