Seyls API reference
Connect your webstore, accounting tool, or anything else to your outlet. Read and update customers, products, and sales over plain JSON.
Overview
The Seyls API gives every outlet its own keys, its own data, and nothing else. A key created for an outlet can only ever see and change that outlet.
All requests use the base URL below, send and receive JSON, and require HTTPS.
https://seyls.com/api/v1
Send an Accept: application/json header with every request. A quick first call:
curl https://seyls.com/api/v1/customers \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Accept: application/json"
Authentication
Create keys in the app under Settings > API Keys. Each key belongs to the outlet it was created in and carries the permissions you picked:
| Permission | Allows |
|---|---|
| customers:read | List and read customers |
| customers:write | Create, update, and archive customers |
| products:read | List and read products |
| products:write | Create, update, and archive products |
| sales:read | List and read sales |
Pass the key as a bearer token on every request:
Authorization: Bearer YOUR_API_KEY
We show the key in full exactly once, right after you create it. Store it in your server's secret store and never ship it in a browser or mobile app. Revoking a key in Settings stops it working immediately, and keys can be created with an expiry date.
Rate limits
Each outlet can make 60 requests per minute, counted across all of its keys. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining headers. Past the limit we answer 429 with a Retry-After header telling you how many seconds to wait.
Errors
Errors are JSON with a human readable message:
{"message": "Resource not found."}
Validation failures list the problems per field:
{
"message": "The name field is required.",
"errors": {"name": ["The name field is required."]}
}
| Status | Meaning |
|---|---|
| 401 | Missing, revoked, or expired key, or a key of the wrong type |
| 403 | The key does not have the required permission |
| 404 | No such record in this outlet |
| 422 | Validation failed, see the errors object |
| 429 | Rate limit reached, retry after the given delay |
| 500 | Something failed on our side |
Pagination
List endpoints are paginated. Pass page and per_page (default 25, maximum 100). Responses wrap records in data with links and meta for paging:
{
"data": [ ... ],
"links": {"first": "...", "last": "...", "prev": null, "next": "..."},
"meta": {"current_page": 1, "last_page": 4, "per_page": 25, "total": 96}
}
Identifiers
Every record has an id that is an opaque string, for example "3RmKq8ZlYVxE". Ids are stable for the life of the record and are the only way to address it. They are not numbers and carry no meaning you can parse, so store them as strings.
Customers
A customer object:
{
"id": "3RmKq8ZlYVxE",
"name": "Aminath Waheeda",
"email": "aminath@example.com",
"phone": "+960 7771234",
"address": "M. Blue House, Male'",
"tin": null,
"notes": null,
"credit_limit": 5000.0,
"status": "active",
"created_at": "2026-05-02T09:14:07+05:00",
"updated_at": "2026-08-01T16:40:22+05:00"
}
/customersLists customers, newest first. Needs customers:read.
| Parameter | Description |
|---|---|
| search optional | Matches name, phone, email, or TIN |
| status optional | active (default) or archived |
| page, per_page optional | See pagination |
/customers/{id}Returns one customer. Needs customers:read.
/customersCreates a customer and returns it with status 201. Needs customers:write.
| Field | Description |
|---|---|
| name required | Customer name |
| email optional | A valid email address |
| phone optional | Contact number |
| address optional | Address line |
| tin optional | Maldives TIN in the standard format |
| notes optional | Free text notes |
| credit_limit optional | Number, defaults to 0 |
curl -X POST https://seyls.com/api/v1/customers \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"name": "Aminath Waheeda", "phone": "+960 7771234"}'
/customers/{id}Updates the fields you send and leaves the rest untouched. Same fields as create. Needs customers:write.
/customers/{id}Archives the customer and answers 204. Sales history stays intact, and the customer can be restored inside the app. Needs customers:write.
Products
A product object:
{
"id": "Xw2PqL9dKm4T",
"code": "SKU-0042",
"name": "Coconut Water 500ml",
"description": null,
"barcode": "8964000551234",
"unit": "PCS",
"cost_price": 8.5,
"selling_price": 12.0,
"tax": {"name": "GST", "rate": 8.0},
"stock": {"tracked": true, "quantity": 240.0, "reorder_level": 24.0, "reorder_quantity": 48.0},
"images": [
{
"url": "https://cdn.seyls.com/product_pictures/8f2k.jpg",
"thumbnail": "https://cdn.seyls.com/product_pictures/thumb_8f2k.jpg"
}
],
"units": [{"unit": "PCS", "price": 12.0, "quantity": 1.0}],
"status": "active",
"created_at": "2026-03-11T10:02:44+05:00",
"updated_at": "2026-08-09T18:21:03+05:00"
}
Prices exclude tax. Stock quantities are read only through the API: they change through sales, goods received, and stock adjustments inside Seyls, so every movement stays accounted for. Product photos are uploaded inside Seyls; the API lists their url and thumbnail links, ready to show on a webstore.
/productsLists products, newest first. Needs products:read.
| Parameter | Description |
|---|---|
| search optional | Matches code, name, or barcode |
| status optional | active (default) or archived |
| page, per_page optional | See pagination |
/products/{id}Returns one product. Needs products:read.
/productsCreates a product and returns it with status 201. Needs products:write.
| Field | Description |
|---|---|
| code required | Product code, unique within your business |
| name required | Product name |
| unit required | Unit of measure, for example PCS or KG |
| cost_price required | Cost per unit, up to 4 decimals |
| selling_price required | Selling price per unit, excluding tax |
| tax_rate required | Must match one of the outlet's active tax rates, use 0 for no tax if a zero rate is configured |
| barcode optional | Barcode |
| reorder_level optional | Stock level that flags a reorder |
| reorder_quantity optional | Quantity to reorder |
/products/{id}Updates the fields you send. Available: name, barcode, reorder_level, reorder_quantity, tax_rate, and track_stock (boolean). Prices, code, and units are managed inside Seyls where price change history is recorded. Needs products:write.
/products/{id}Archives the product and answers 204. It disappears from sale screens but keeps its history, and can be restored inside the app. Needs products:write.
Sales
Sales are read only through the API. A sale in Seyls is rung up at the register by a signed in user with an open cash drawer, so creating one belongs to the POS. The API is how the rest of your tools see them.
A sale object:
{
"id": "kX9mP2wQ4z",
"number": "INV-2026-01184",
"type": "credit",
"date": "09/08/2026",
"due_date": "2026-09-08",
"reference": "PO-556",
"customer": {"id": "3RmKq8ZlYVxE", "name": "Aminath Waheeda"},
"currency": "MVR",
"exchange_rate": 1.0,
"amounts": {"subtotal": 1150.0, "tax": 92.0, "discount": 0.0, "rounding": 0.0, "total": 1242.0},
"paid": false,
"balance_due": 742.0,
"items": [
{
"product": {"id": "Xw2PqL9dKm4T", "code": "SKU-0042", "name": "Coconut Water 500ml"},
"quantity": 10.0,
"unit": "PCS",
"unit_price": 12.0,
"discount": 0.0,
"tax_rate": 8.0,
"tax": 9.6,
"line_total": 120.0
}
],
"created_at": "2026-08-09T14:03:51+05:00",
"updated_at": "2026-08-09T14:03:51+05:00"
}
Amounts are in the outlet's base currency. For sales made in another currency, currency and exchange_rate describe the sale currency and the rate frozen at the time of sale. type is one of cash, credit, hold, refunded, or quote. paid is true for cash sales, reflects the credit balance for credit sales, and is null for other document types. The date field follows the outlet's configured date format; use created_at when you need a machine readable timestamp.
/salesLists sales, newest first. Without a type filter we return completed documents: cash, credit, and refunded. Line items are only included on the single sale endpoint. Needs sales:read.
| Parameter | Description |
|---|---|
| type optional | cash, credit, hold, refunded, or quote |
| from optional | Include sales created on or after this date, format YYYY-MM-DD |
| to optional | Include sales created on or before this date, format YYYY-MM-DD |
| customer optional | A customer id, returns that customer's sales |
| page, per_page optional | See pagination |
/sales/{id}Returns one sale with its line items. Needs sales:read.
/sales/{id}/pdfDownloads the sale's invoice as a PDF, rendered with the same template, logo, and numbering your printed invoices use. The response is the PDF itself with Content-Type: application/pdf. Held bills answer 422 because they have no invoice document yet. Needs sales:read.
curl -o invoice.pdf https://seyls.com/api/v1/sales/kX9mP2wQ4z/pdf \ -H "Authorization: Bearer YOUR_API_KEY"
Need something the API does not cover?
Tell us what you are building through the contact page. We grow the API from real integrations, the same way we grow Seyls itself.