Core concepts
Making requests
JSON bodies, required headers and the standard response envelope.
The API speaks JSON over HTTPS with conventional REST verbs: GET to read, POST to create, PATCH to update, and DELETE to remove. Send Content-Type: application/json on any request with a body.
The response envelope
Every successful response is wrapped in a consistent envelope. The resource you asked for is always under data; meta carries the request id (also returned in the X-Request-Id response header) and a server timestamp.
{
"data": { /* the resource, or an array of resources */ },
"meta": {
"requestId": "req_9f3a...",
"timestamp": "2026-09-06T09:00:00.000Z"
}
}Errors use a different, flat shape (see errors) — they are not wrapped in data.
Request headers
| Header | Required | Purpose |
|---|---|---|
Authorization | Yes (protected routes) | Bearer <access token> from login. |
Content-Type | On writes | Always application/json. |
X-Company-Id | Recommended | Selects the active company for tenants with more than one. Data is isolated per company; without it the request falls back to the caller's default scope. |
Idempotency-Key | Optional | Makes a write safely retryable — see below. |
Choosing the active company
?companyId=query parameter for simple links and GETs that cannot set a header. The header wins when both are present. The server still verifies the caller's membership actually grants access to that company.Pagination & filtering
List endpoints accept a common set of query parameters. Results are returned newest-first by default.
| Parameter | Default | Notes |
|---|---|---|
page | 1 | 1-based page number. |
limit | 20 | Rows per page, maximum 1000. |
search | — | Free-text match across the resource's searchable fields. |
sortBy / sortDir | desc | Sort column (whitelisted per resource; an unknown column falls back to newest-first) and direction (asc or desc). |
status | — | Filter documents by status, e.g. Draft / Posted. |
customerId / vendorId | — | Filter rows by their linked customer or vendor (ledger views). |
paymentType | — | Cash / Credit — sales and purchase invoices. |
curl "https://gulfpay-production.up.railway.app/api/v1/customers?page=1&limit=20&search=acme&sortDir=desc" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "X-Company-Id: cmp_123"For the full set of paging, sorting and filter parameters — and how to iterate every page — see pagination & filtering.
Idempotent writes
Any mutating request (POST, PATCH, PUT, DELETE) may carry an Idempotency-Key header — a unique value you generate per logical operation (a UUID is ideal). If a request is retried with the same key — for example after a dropped connection where you never saw the response — the server recognises the key and does not apply the change a second time.
curl -X POST https://gulfpay-production.up.railway.app/api/v1/customers \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "X-Company-Id: cmp_123" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5f0b2b1e-8a1c-4c2b-9f3a-0d1e2f3a4b5c" \
-d '{ "name": "Acme Trading LLC", "email": "ap@acme.example" }'The first request with a given key is processed normally and the key is recorded once it succeeds (a failed request stays retryable). A replay of an already-applied key returns 409 Conflict with { "idempotentReplay": true } — treat that as success and stop retrying. Keys are retained for 7 days.
Generate a fresh key per operation
Body size
JSON bodies may be up to 50 MB, which comfortably covers base64-encoded attachments such as document PDFs and item images.