Developers
The Dryline VMS API.
Connect your own systems to your machines, layouts, stock, products and sales.
Quick start
Make a key and call the API.
- In the Dryline console, open Pro, then API access, and make a key. Owners and admins can make keys.
- Copy it when it is shown. It is shown once.
- Send it as
Authorization: Bearer <key>tohttps://app.drylinevms.com/api/v1. - For more rows, pass the answer’s
nextCursorascursor.
The full reference is the OpenAPI document, which most API tools can import.
curl https://app.drylinevms.com/api/v1/sales?limit=100 \ -H "Authorization: Bearer dl_live_…"
{
"data": [
{
"id": "6d1c…",
"status": "completed",
"tender": "card",
"occurredAt": "2026-10-14T18:02:11.000Z",
"machineId": "a3f0…",
"slot": "B4",
"productName": "Sparkling Water 12oz",
"amountCents": 225,
"taxCents": 17,
"costCents": 61,
"currency": "USD"
}
],
"nextCursor": "MjAyNi0xMC0xNFQx…"
}Endpoints
What you can read.
| GET /locations | Your locations and each one’s time zone |
| GET /machines | Your machines and what needs attention at each |
| GET /machines/{id} | One machine |
| GET /machines/{id}/planogram | The published layout: slot, product, price, capacity, par level |
| GET /machines/{id}/dex-readings | DEX audit reads (add include=raw for the file itself) |
| GET /inventory | Stock in every slot |
| GET /products | Your product catalog |
| GET /sales | Every item vended, with tax, cost and cash or card |
| GET /sales/summary | Sales totals for a period in your local time |
| GET /cash-collections | Cash taken out of machines |
| GET /routes | Driver routes and their stops |
| GET /service-visits | Machines marked serviced |
| GET /warehouse/stock | Warehouse stock per product |
Changes
What a read and write key can change.
Stock changes need an Idempotency-Key header. Send the same change again with the same key and nothing is counted twice: the answer is 200 with applied: false.
| POST /products | Add a product. A barcode (upc) or SKU is required. |
| PATCH /products/{id} | Change or archive a product (active: false) |
| POST /machines/{id}/inventory-entries | Restock, correct or count a slot |
| POST /warehouse/receipts | Receive stock into the warehouse |
curl -X POST https://app.drylinevms.com/api/v1/machines/{id}/inventory-entries \
-H "Authorization: Bearer dl_live_…" \
-H "Idempotency-Key: po-1001-a1" \
-H "Content-Type: application/json" \
-d '{"slot":"A1","type":"restock","units":6,"bestBy":"2027-01-31"}'Webhooks
Told when it happens.
Add an https address under API access and pick its events. Each event is sent within about a minute as { id, type, createdAt, data }, where data is the same record the API returns. If your side is down it is sent again for about two days. The same event can arrive twice: use its id.
| sale.completed | A vend whose payment settled |
| sale.reversed | A sale voided or refunded |
| dex.received | A machine sent its DEX audit file |
| cash_collection.created | Cash taken out of a machine |
| service_visit.created | A driver marked a machine serviced |
Every message carries a Dryline-Signature header. Check it with the signing secret shown when you added the address:
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: the request body exactly as received, before JSON.parse.
function isFromDryline(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 ?? '', 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
}Keys, limits and formats
Keys
A key is either read only or read and write. Make one per system you connect, so one can be revoked without the others. Up to 10 at a time.
Limits
120 requests a minute per key, and 300 per account across its keys. Each answer carries X-RateLimit headers.
Money and time
Money is in whole cents of US dollars. Times are ISO-8601 in UTC. A value we do not know is null.
Pages
Long lists come in pages of up to 200 rows, in the order Dryline recorded them, so a late sale is never skipped.
Sales that change
A sale can settle, or be voided or refunded, after it is recorded. Ask /sales with updatedSince to pick those changes up.
Versions
Version 1 only gains fields. Ignore fields you do not know.
Errors
Every error has a code.
Errors are RFC 9457 problem details: a status, a code to branch on, a sentence, and the requestId.
| missing_api_key | 401 | No Authorization header with a key. |
| invalid_api_key | 401 | The key is not valid. It may have been revoked. |
| wrong_environment | 401 | A test key was sent to production, or the other way round. |
| insufficient_scope | 403 | A read only key was used to change data. |
| plan_required | 403 | The account is not on DrylineVMS Pro. |
| account_suspended | 403 | The account is suspended. |
| account_canceled | 403 | The account is canceled. |
| vending_not_enabled | 403 | Vending is not turned on for the account. |
| wrong_host | 403 | The key was sent to another business’s address. Use app.drylinevms.com. |
| not_found | 404 | No such record for this business. |
| already_exists | 409 | A product with that barcode or SKU exists. existingId is its id. |
| invalid_request | 400 | A parameter is not valid. The errors list names each one. |
| rate_limited | 429 | Over the limit. Wait the Retry-After seconds. |
| internal_error | 500 | Something went wrong on our side. Quote the requestId. |
Building an integration?
Tell us what you are connecting and we will help you map it.