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

  1. Create a token in the admin panel (see below) and copy it somewhere safe.

  2. 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"
    
  3. Read the JSON. A successful response has "status": true and your data under data. 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

  1. Open Setup → REST API tokens (/admin/tsync_api_tokens). You can also press Ctrl/⌘+K and search "API".
  2. Enter a label (e.g. "Mobile App", "Zapier integration") so you can recognise it later.
  3. 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.
  4. 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


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.