REST API
The TSync REST API lets your own code talk to TSync. With it you can pull data into another system, build a custom dashboard or mobile app, keep two systems in sync, or wire TSync into automation tools — all over plain HTTPS with JSON.
If you've used any modern web API before, this will feel familiar: you get a token, send it on every request, and get clean JSON back. This page walks you through it, then lists every endpoint with example requests and responses.
Base URL: https://your-domain.com/api/v1
Every path below is relative to that base. So /invoices really means
https://your-domain.com/api/v1/invoices.
Getting started in three steps
Create a token in the admin panel (see below) and copy it somewhere safe.
Send it on a request. The quickest smoke test — list your invoices:
curl -s -H "Authorization: Bearer YOUR_TOKEN" \ "https://your-domain.com/api/v1/invoices?per_page=5"Read the JSON. A successful response has
"status": trueand your data underdata. That's the whole loop — everything else is just more endpoints and parameters.
Authentication
Every request must carry a Bearer token in the Authorization header.
Generating a token
- Open Setup → REST API tokens (
/admin/tsync_api_tokens). You can also press Ctrl/⌘+K and search "API". - Enter a label (e.g. "Mobile App", "Zapier integration") so you can recognise it later.
- Choose the staff member the token acts as. The token inherits that person's permissions — it can do exactly what they can do, no more.
- Click Generate token. The token is shown once — copy and store it securely. You won't be able to see it again (only a hash is kept).
Using the token
Include it in every request:
Authorization: Bearer your-api-token-here
Because a token inherits its staff member's permissions, access lines up with the
admin UI. If that person can't view invoices in TSync, the token can't reach
/invoices either — it gets a 403.
Revoking a token
Go to Setup → REST API tokens, find the token in the list (label, owner, last used, expiry, status), and click Revoke. It stops working immediately.
Rate limiting
- 100 requests per minute per token.
- Every response includes headers so you can pace yourself:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
Max requests per window (e.g. 100) |
X-RateLimit-Remaining |
Requests left in the current window |
X-RateLimit-Reset |
Unix timestamp when the window resets |
Go over the limit and you get 429 Too Many Requests. The body tells you how
long to wait:
{
"status": false,
"error": "rate_limit_exceeded",
"message": "Too many requests. Retry after 23 seconds.",
"retry_after": 23
}
A simple, polite client watches X-RateLimit-Remaining and backs off, or retries
after retry_after seconds when it sees a 429.
Response format
Every response uses the same JSON shape, so you can handle them all the same way.
A single item comes back under data:
{
"status": true,
"data": {
"id": 123,
"number": "INV-000123",
"total": 5000.00
}
}
A list comes back as an array under data, with a meta block for paging:
{
"status": true,
"data": [
{ "id": 1, "...": "..." },
{ "id": 2, "...": "..." }
],
"meta": {
"page": 1,
"per_page": 25,
"total": 142,
"total_pages": 6
}
}
An error always has "status": false, a short error code you can branch on,
and a human-readable message:
{
"status": false,
"error": "not_found",
"message": "Invoice #999 not found."
}
Tip: check status first. If it's true, read data; if it's false, read
error and message.
Pagination
Every list endpoint is paginated:
| Parameter | Default | Meaning |
|---|---|---|
page |
1 | Page number (1-based) |
per_page |
25 | Items per page (max 100) |
Use the total_pages value in meta to know when to stop.
Example: GET /api/v1/invoices?page=2&per_page=50
Filtering and sorting
List endpoints accept query parameters to narrow and order results:
| Parameter | Applies to | Meaning |
|---|---|---|
search |
All | Text search across key fields |
status |
Invoices, Leads | Filter by status (e.g. paid, unpaid, overdue) |
date_from |
Anything dated | Start date (YYYY-MM-DD) |
date_to |
Anything dated | End date (YYYY-MM-DD) |
client_id |
Invoices, Payments | Filter by client |
assigned |
Leads | Filter by assigned staff ID |
sort |
All | Field to sort by (e.g. date, total, name) |
order |
All | asc or desc |
Example: GET /api/v1/invoices?status=unpaid&date_from=2026-01-01&sort=total&order=desc
Endpoints
The API covers your core business records. Here's each one with its parameters and a sample response.
Invoices
List invoices
GET /api/v1/invoices
Query parameters: status, client_id, date_from, date_to, search,
page, per_page
Response:
{
"status": true,
"data": [
{
"id": 123,
"number": "INV-000123",
"client_id": 45,
"client_name": "Acme SRL",
"date": "2026-05-01",
"duedate": "2026-05-31",
"subtotal": 4201.68,
"total_tax": 798.32,
"total": 5000.00,
"currency": "RON",
"status": "unpaid",
"status_label": "Unpaid"
}
],
"meta": { "page": 1, "per_page": 25, "total": 142, "total_pages": 6 }
}
Get a single invoice
GET /api/v1/invoices/:id
Returns the full invoice, including its line items and any recorded payments.
{
"status": true,
"data": {
"id": 123,
"number": "INV-000123",
"client_id": 45,
"client_name": "Acme SRL",
"date": "2026-05-01",
"duedate": "2026-05-31",
"subtotal": 4201.68,
"total_tax": 798.32,
"total": 5000.00,
"currency": "RON",
"status": "unpaid",
"items": [
{
"description": "Consulting services — May 2026",
"qty": 40,
"rate": 100.00,
"tax_name": "TVA 19%",
"tax_rate": 19.00,
"amount": 4000.00
}
],
"payments": [],
"created_at": "2026-05-01T10:30:00+03:00"
}
}
Clients
List clients
GET /api/v1/clients
Query parameters: search, page, per_page
Response:
{
"status": true,
"data": [
{
"id": 45,
"company": "Acme SRL",
"vat": "RO12345678",
"phonenumber": "+40721000000",
"city": "Bucharest",
"country": "Romania",
"active": true,
"total_invoiced": 125000.00,
"health_score": 82
}
],
"meta": { "page": 1, "per_page": 25, "total": 310, "total_pages": 13 }
}
Get a single client
GET /api/v1/clients/:id
Returns full client details including contacts, notes, and summary statistics.
Leads
List leads
GET /api/v1/leads
Query parameters: status, source, assigned, search, date_from,
date_to, page, per_page
Response:
{
"status": true,
"data": [
{
"id": 789,
"name": "John Smith",
"company": "Widget Corp",
"email": "john@widgetcorp.com",
"phonenumber": "+40722000000",
"value": 15000.00,
"status": "new",
"source": "Website",
"assigned": 3,
"assigned_name": "Maria Popescu",
"created_at": "2026-05-15T14:20:00+03:00"
}
],
"meta": { "page": 1, "per_page": 25, "total": 58, "total_pages": 3 }
}
Create a lead
POST /api/v1/leads
Content-Type: application/json
Request body:
{
"name": "Jane Doe",
"company": "NewCo SRL",
"email": "jane@newco.ro",
"phonenumber": "+40723000000",
"value": 8000.00,
"source": "API",
"assigned": 3,
"description": "Interested in warehouse module"
}
Response (201 Created):
{
"status": true,
"data": {
"id": 790,
"name": "Jane Doe",
"company": "NewCo SRL",
"status": "new",
"created_at": "2026-05-16T09:00:00+03:00"
}
}
Stock
List stock items
GET /api/v1/stock
Query parameters: search, warehouse_id, below_reorder (boolean), page,
per_page
Response:
{
"status": true,
"data": [
{
"id": 201,
"code": "MAT-001",
"name": "Steel Rod 10mm",
"warehouse_id": 1,
"warehouse_name": "Main Warehouse",
"qty_on_hand": 450,
"qty_reserved": 30,
"qty_available": 420,
"reorder_level": 100,
"unit": "kg",
"avg_cost": 12.50,
"total_value": 5625.00
}
],
"meta": { "page": 1, "per_page": 25, "total": 1200, "total_pages": 48 }
}
Stock movements
GET /api/v1/stock/movements
Query parameters: item_id, warehouse_id, type (in/out/transfer),
date_from, date_to, page, per_page
Response:
{
"status": true,
"data": [
{
"id": 5001,
"item_id": 201,
"item_name": "Steel Rod 10mm",
"type": "in",
"qty": 100,
"reference_type": "grn",
"reference_id": 88,
"warehouse_id": 1,
"date": "2026-05-14T08:00:00+03:00",
"staff_id": 2,
"staff_name": "Ion Popescu"
}
],
"meta": { "page": 1, "per_page": 25, "total": 8400, "total_pages": 336 }
}
Payments
List payments
GET /api/v1/payments
Query parameters: client_id, invoice_id, date_from, date_to, page,
per_page
Response:
{
"status": true,
"data": [
{
"id": 567,
"invoice_id": 123,
"invoice_number": "INV-000123",
"client_id": 45,
"amount": 5000.00,
"payment_mode": "Bank Transfer",
"date": "2026-05-10",
"note": "Wire transfer ref: TRF-2026-0510"
}
],
"meta": { "page": 1, "per_page": 25, "total": 890, "total_pages": 36 }
}
Expenses
List expenses
GET /api/v1/expenses
Query parameters: category, date_from, date_to, billable (boolean),
search, page, per_page
Response:
{
"status": true,
"data": [
{
"id": 340,
"category": "Office Supplies",
"amount": 250.00,
"currency": "RON",
"tax": 47.50,
"date": "2026-05-12",
"client_id": null,
"billable": false,
"reference_no": "RCP-2026-0512",
"note": "Printer cartridges"
}
],
"meta": { "page": 1, "per_page": 25, "total": 234, "total_pages": 10 }
}
Staff
List staff
GET /api/v1/staff
Query parameters: search, role, active (boolean), page, per_page
Response:
{
"status": true,
"data": [
{
"id": 3,
"firstname": "Maria",
"lastname": "Popescu",
"email": "maria@company.ro",
"role": "Senior Sales",
"active": true,
"last_login": "2026-05-16T08:45:00+03:00"
}
],
"meta": { "page": 1, "per_page": 25, "total": 18, "total_pages": 1 }
}
The token's own user
GET /api/v1/staff/me
Handy for confirming a token works and seeing what it's allowed to do. Returns the staff member behind the token, their permissions, and the current rate-limit status.
{
"status": true,
"data": {
"id": 3,
"firstname": "Maria",
"lastname": "Popescu",
"email": "maria@company.ro",
"role": "Senior Sales",
"permissions": ["invoices.view", "clients.view", "leads.manage", "stock.view"],
"token_description": "Mobile App",
"rate_limit": 100,
"rate_limit_remaining": 87
}
}
Analytics datasets
List datasets
GET /api/v1/analytics/datasets
Lists the built-in analytics datasets you can run. Each one has a stable string id,
a human-readable label, and a short description.
Response:
{
"status": true,
"data": [
{ "id": "revenue_monthly", "label": "Monthly Revenue", "description": "Total invoiced revenue grouped by month." },
{ "id": "leads_by_status", "label": "Leads by Status", "description": "Count of leads grouped by their current status." },
{ "id": "expenses_by_category", "label": "Expenses by Category", "description": "Total expenses grouped by category." },
{ "id": "invoices_aging", "label": "Invoices Aging", "description": "Unpaid invoices grouped by aging buckets." },
{ "id": "stock_valuation", "label": "Stock Valuation", "description": "Current stock quantities and values by warehouse." }
]
}
Run a dataset
GET /api/v1/analytics/run/:dataset_id
Runs one of the datasets above (the :dataset_id is the id from the list) and returns
its computed rows. Each dataset checks the matching permission (e.g. revenue_monthly
needs invoice view rights).
{
"status": true,
"data": {
"dataset": "revenue_monthly",
"results": [
{ "month": "2026-05", "revenue": 184200.00, "invoice_count": 37 }
],
"generated_at": "2026-05-16 02:00:00"
}
}
More endpoints
A few more read endpoints round out the API. They follow the same envelope, token and pagination rules as the ones above.
| Endpoint | Verb | Returns |
|---|---|---|
/api/v1/status |
GET |
Service health and the identity behind your token (also the response for GET /api/v1). |
/api/v1/items |
GET |
Catalogue items (list). search, page, per_page. |
/api/v1/items/:id |
GET |
A single catalogue item. |
/api/v1/treasury/places |
GET |
Money-holding places — read-only, scoped to the token's companies. |
/api/v1/bank/accounts |
GET |
Bank accounts — read-only, scoped to the token's companies. |
Some resources also accept writes when the token's staff member has the right permission:
POST /api/v1/invoices, POST /api/v1/clients, plus GET /api/v1/leads/:id and
PUT /api/v1/leads/:id alongside the lead endpoints shown above.
Error codes
When status is false, the HTTP status and error code tell you what went
wrong:
| HTTP status | Error code | Meaning |
|---|---|---|
| 400 | bad_request |
Malformed request or invalid parameters |
| 401 | unauthorized |
Missing or invalid token |
| 403 | forbidden |
Token is valid but lacks permission for this resource |
| 404 | not_found |
Resource does not exist |
| 422 | validation_error |
Input failed validation (details in the errors object) |
| 429 | rate_limit_exceeded |
Too many requests |
| 500 | server_error |
Something went wrong on the server |
A validation error spells out exactly which fields to fix:
{
"status": false,
"error": "validation_error",
"message": "Validation failed.",
"errors": {
"email": ["The email field is required."],
"name": ["The name must be at least 2 characters."]
}
}
Code examples
The same two tasks — list unpaid invoices and create a lead — in three languages.
cURL
# List unpaid invoices
curl -s -H "Authorization: Bearer YOUR_TOKEN" \
"https://erp.example.com/api/v1/invoices?status=unpaid&per_page=10"
# Get a single client
curl -s -H "Authorization: Bearer YOUR_TOKEN" \
"https://erp.example.com/api/v1/clients/45"
# Create a lead
curl -s -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Jane Doe","email":"jane@example.com","value":5000,"source":"API"}' \
"https://erp.example.com/api/v1/leads"
PHP
<?php
$token = 'YOUR_TOKEN';
$base = 'https://erp.example.com/api/v1';
// List invoices
$ch = curl_init("{$base}/invoices?status=unpaid&per_page=50");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
foreach ($response['data'] as $invoice) {
echo "#{$invoice['number']} — {$invoice['total']} {$invoice['currency']}\n";
}
// Create a lead
$ch = curl_init("{$base}/leads");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$token}",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Jane Doe',
'email' => 'jane@example.com',
'value' => 5000,
'source' => 'API',
]),
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
echo "Created lead #{$result['data']['id']}\n";
JavaScript (fetch)
const TOKEN = 'YOUR_TOKEN';
const BASE = 'https://erp.example.com/api/v1';
// List invoices
const invoices = await fetch(`${BASE}/invoices?status=unpaid`, {
headers: { 'Authorization': `Bearer ${TOKEN}` }
}).then(r => r.json());
console.log(`Found ${invoices.meta.total} unpaid invoices`);
invoices.data.forEach(inv => {
console.log(`#${inv.number} — ${inv.total} ${inv.currency}`);
});
// Create a lead
const newLead = await fetch(`${BASE}/leads`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Jane Doe',
email: 'jane@example.com',
value: 5000,
source: 'API'
})
}).then(r => r.json());
console.log(`Created lead #${newLead.data.id}`);
Webhooks (coming soon)
Push notifications when records change are planned for a future release. For now,
poll with the date_from filter to pick up new or updated records since you last
checked.
Permissions
API access uses the same permission system as the admin UI. The relevant ones:
| Permission | Needed to |
|---|---|
| Manage API tokens | Generate and revoke tokens |
| API access | Use the API at all (the baseline) |
| Per-record permissions | Reach each endpoint (e.g. view invoices, create leads) |
Because tokens inherit their staff member's role, the simplest way to control what a token can do is to point it at a staff member with exactly the right permissions.
See also
- Automations — react to data changes without writing code
- Getting started with TSync — the product overview
Response format (v5.9.17)
Every response uses one envelope:
Success (single object):
{ "status": true, "data": { } }
Success (list):
{ "status": true, "data": [ ], "meta": { "page": 1, "per_page": 25, "total": 100, "total_pages": 4 } }
Error:
{ "status": false, "error": "not_found", "message": "…" }
(ok is also present alongside status for older clients.) Every response carries rate-limit headers — X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset — and every call is logged (token, staff, company, method, endpoint, status, duration, IP).
Endpoints
GET /api/v1/status · GET /api/v1/staff/me · GET|POST /api/v1/clients · GET|POST /api/v1/leads, PUT /api/v1/leads/{id} · GET /api/v1/items · GET /api/v1/stock · GET /api/v1/treasury/places (read-only) · GET /api/v1/bank/accounts (read-only) · plus invoices, payments, expenses, analytics.
Each token belongs to a staff member and respects that staff member's permissions; data is never returned across companies the token may not access.