The Xuvilo API lets your own software read and update a workspace's clients, products and saved documents, and follow the documents you share with tracking links. It speaks JSON over HTTPS, with one API key per integration.
Every request carries an API key in the Authorization header, as Bearer followed by the key. A key starts with xv_ and is followed by 43 letters, digits, hyphens and underscores.
Create keys in your Dashboard, on the API Keys tab. You need to be the workspace's owner or an admin. A key is shown once, when you create it: copy it then. Xuvilo keeps only a fingerprint of it (a SHA-256 hash), so a lost key cannot be shown again. Revoke it and create a new one.
A key belongs to one workspace and reaches only that workspace's data. What a key changes is recorded as changed by the person who created it.
Treat a key like a password. Keep it on your server, in an environment variable or a secrets store, never in a web page, an app you ship to customers or a code repository. If one leaks, revoke it on the API Keys tab; it stops working at once.
export XUVILO_API_KEY="xv_…"
curl https://xuvilo.com/api/v1/workspace \
-H "Authorization: Bearer $XUVILO_API_KEY"
Each key has one of two scopes, chosen when it is created. A read key can call every GET endpoint. A write key can also create, change and delete records and cancel or reopen tracked documents. A read key that tries to write is answered 403 insufficient_scope.
Give each integration the smallest scope it needs: a report that only reads gets a read key.
The base URL is https://xuvilo.com/api/v1. Send JSON with Content-Type: application/json; a body sent with any other Content-Type, or none, is answered 400 invalid_request. Every successful answer is an object with the result under data, and a list adds pagination: limit, offset and the total number of records.
To read a long list, ask again with offset increased by limit until offset reaches total. Ids are strings. Times are ISO 8601 in UTC. Amounts and prices are decimal strings, such as "1250.5" or "150", so no digit is lost to floating point. They come in their shortest form, without trailing zeros, so format them for display with the decimals of their currency. Tax percentages (taxPct) are numbers. You may send amounts and prices as strings or as numbers.
A refusal carries an error object instead, with a code your program can test and a message for a person. The full list is under Errors below.
{
"error": {
"code": "insufficient_scope",
"message": "This key can only read. Use a key with the write scope."
}
}
The workspace the key belongs to. A good first call: it proves the key works and tells you which business it reaches.
The key's workspace: its id, its name, the owner's name and the key's scope. Scope: read.
curl https://xuvilo.com/api/v1/workspace \
-H "Authorization: Bearer $XUVILO_API_KEY"
Answer (200):
{
"data": {
"id": "0b7e4c19-5a2d-4f63-9e81-3c6d2a0f8b45",
"name": "Haddad Design Studio",
"ownerName": "Omar Haddad",
"scope": "read"
}
}
The address book the editors fill the Bill To block from.
List the workspace's clients, newest first. Scope: read.
limit: How many records to return, 1–100. Default 50.offset: How many records to skip from the start. Default 0.curl "https://xuvilo.com/api/v1/clients?limit=20" \
-H "Authorization: Bearer $XUVILO_API_KEY"
Answer (200):
{
"data": [
{
"id": "6f1c2a8e-3d4b-4c55-9a43-1b2f7c9d0e11",
"name": "Layla Haddad",
"company": "Haddad Trading",
"email": "[email protected]",
"phone": "+962 6 555 0142",
"address": "12 Al-Madina Street",
"city": "Amman",
"country": "Jordan",
"taxId": "",
"notes": "",
"createdAt": "2026-10-01T09:12:44.000Z",
"updatedAt": "2026-10-01T09:12:44.000Z"
}
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 1
}
}
Create a client. Answers 201 with the new record. Scope: write.
curl -X POST https://xuvilo.com/api/v1/clients \
-H "Authorization: Bearer $XUVILO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Karim Mansour","company":"Mansour Logistics","email":"[email protected]"}'
Answer (201):
{
"data": {
"id": "e4b7a913-2c58-4d0f-a6e2-7f3b1c9d5a82",
"name": "Karim Mansour",
"company": "Mansour Logistics",
"email": "[email protected]",
"phone": "",
"address": "",
"city": "",
"country": "",
"taxId": "",
"notes": "",
"createdAt": "2026-10-04T13:58:21.000Z",
"updatedAt": "2026-10-04T13:58:21.000Z"
}
}
One client by its id. Scope: read.
curl https://xuvilo.com/api/v1/clients/6f1c2a8e-3d4b-4c55-9a43-1b2f7c9d0e11 \
-H "Authorization: Bearer $XUVILO_API_KEY"
Answer (200):
{
"data": {
"id": "6f1c2a8e-3d4b-4c55-9a43-1b2f7c9d0e11",
"name": "Layla Haddad",
"company": "Haddad Trading",
"email": "[email protected]",
"phone": "+962 6 555 0142",
"address": "12 Al-Madina Street",
"city": "Amman",
"country": "Jordan",
"taxId": "",
"notes": "",
"createdAt": "2026-10-01T09:12:44.000Z",
"updatedAt": "2026-10-01T09:12:44.000Z"
}
}
Change some fields of a client. Fields you leave out keep their values. Scope: write.
curl -X PATCH https://xuvilo.com/api/v1/clients/6f1c2a8e-3d4b-4c55-9a43-1b2f7c9d0e11 \
-H "Authorization: Bearer $XUVILO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"+962 6 555 0199"}'
Answer (200):
{
"data": {
"id": "6f1c2a8e-3d4b-4c55-9a43-1b2f7c9d0e11",
"name": "Layla Haddad",
"company": "Haddad Trading",
"email": "[email protected]",
"phone": "+962 6 555 0199",
"address": "12 Al-Madina Street",
"city": "Amman",
"country": "Jordan",
"taxId": "",
"notes": "",
"createdAt": "2026-10-01T09:12:44.000Z",
"updatedAt": "2026-10-04T14:05:00.000Z"
}
}
Delete a client. This cannot be undone. Scope: write.
curl -X DELETE https://xuvilo.com/api/v1/clients/6f1c2a8e-3d4b-4c55-9a43-1b2f7c9d0e11 \
-H "Authorization: Bearer $XUVILO_API_KEY"
Answer (200):
{
"data": {
"id": "6f1c2a8e-3d4b-4c55-9a43-1b2f7c9d0e11",
"deleted": true
}
}
The catalogue the editors' line-item picker reads. A price is a decimal string with up to four decimals, so three-decimal currencies such as KWD and LYD keep every digit. taxPct is the product's own tax percentage as a number, such as 5 or 7.5, or null when it has none and the document's rate applies.
List the workspace's products, newest first. Scope: read.
limit: How many records to return, 1–100. Default 50.offset: How many records to skip from the start. Default 0.curl "https://xuvilo.com/api/v1/products?limit=20" \
-H "Authorization: Bearer $XUVILO_API_KEY"
Answer (200):
{
"data": [
{
"id": "a2d94b10-7e3c-4f8a-b6d1-5c0e9f2a7b34",
"name": "Website maintenance",
"description": "Monthly updates, backups and security patches",
"unit": "month",
"price": "150",
"currency": "KWD",
"taxPct": null,
"createdAt": "2026-10-02T11:30:00.000Z",
"updatedAt": "2026-10-02T11:30:00.000Z"
}
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 1
}
}
Create a product. Answers 201 with the new record. Scope: write.
curl -X POST https://xuvilo.com/api/v1/products \
-H "Authorization: Bearer $XUVILO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Logo design","unit":"project","price":"85.500","currency":"KWD","taxPct":5}'
Answer (201):
{
"data": {
"id": "e4b7a913-2c58-4d0f-a6e2-7f3b1c9d5a82",
"name": "Logo design",
"description": "",
"unit": "project",
"price": "85.5",
"currency": "KWD",
"taxPct": 5,
"createdAt": "2026-10-04T13:58:21.000Z",
"updatedAt": "2026-10-04T13:58:21.000Z"
}
}
One product by its id. Scope: read.
curl https://xuvilo.com/api/v1/products/a2d94b10-7e3c-4f8a-b6d1-5c0e9f2a7b34 \
-H "Authorization: Bearer $XUVILO_API_KEY"
Answer (200):
{
"data": {
"id": "a2d94b10-7e3c-4f8a-b6d1-5c0e9f2a7b34",
"name": "Website maintenance",
"description": "Monthly updates, backups and security patches",
"unit": "month",
"price": "150",
"currency": "KWD",
"taxPct": null,
"createdAt": "2026-10-02T11:30:00.000Z",
"updatedAt": "2026-10-02T11:30:00.000Z"
}
}
Change some fields of a product. Fields you leave out keep their values. Scope: write.
curl -X PATCH https://xuvilo.com/api/v1/products/a2d94b10-7e3c-4f8a-b6d1-5c0e9f2a7b34 \
-H "Authorization: Bearer $XUVILO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"price":"165.000"}'
Answer (200):
{
"data": {
"id": "a2d94b10-7e3c-4f8a-b6d1-5c0e9f2a7b34",
"name": "Website maintenance",
"description": "Monthly updates, backups and security patches",
"unit": "month",
"price": "165",
"currency": "KWD",
"taxPct": null,
"createdAt": "2026-10-02T11:30:00.000Z",
"updatedAt": "2026-10-04T14:05:00.000Z"
}
}
Delete a product. This cannot be undone. Scope: write.
curl -X DELETE https://xuvilo.com/api/v1/products/a2d94b10-7e3c-4f8a-b6d1-5c0e9f2a7b34 \
-H "Authorization: Bearer $XUVILO_API_KEY"
Answer (200):
{
"data": {
"id": "a2d94b10-7e3c-4f8a-b6d1-5c0e9f2a7b34",
"deleted": true
}
}
Documents saved to the workspace with Save to Dashboard: invoices, quotations, purchase orders, delivery notes, expense reports and receipts. amount is a decimal string. The list leaves out each document's payload; ask for one document to get it.
List the workspace's saved documents, newest first. Scope: read.
type: invoice, quotation, purchase-order, delivery-note, expense-report or receipt.status: A status the type allows, for example draft, sent, paid or cancelled.from: Only documents last edited on or after this date (ISO 8601).to: Only documents last edited on or before this date (ISO 8601).limit: How many records to return, 1–100. Default 50.offset: How many records to skip from the start. Default 0.curl "https://xuvilo.com/api/v1/documents?type=invoice&status=sent&from=2026-10-01" \
-H "Authorization: Bearer $XUVILO_API_KEY"
Answer (200):
{
"data": [
{
"id": "c8e1f5a2-9b4d-4e7f-8a3c-2d6b0e1f9a57",
"type": "invoice",
"title": "INV-2026-0042",
"clientName": "Haddad Trading",
"amount": "1250.5",
"currency": "JOD",
"status": "sent",
"createdAt": "2026-10-03T08:00:00.000Z",
"lastEditedAt": "2026-10-03T08:20:15.000Z"
}
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 1
}
}
Create a saved document. Answers 201 with the new record. Scope: write.
curl -X POST https://xuvilo.com/api/v1/documents \
-H "Authorization: Bearer $XUVILO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"invoice","title":"INV-2026-0043","clientName":"Haddad Trading","amount":"980.250","currency":"JOD","status":"draft","payload":{"invoiceNumber":"INV-2026-0043"}}'
Answer (201):
{
"data": {
"id": "e4b7a913-2c58-4d0f-a6e2-7f3b1c9d5a82",
"type": "invoice",
"title": "INV-2026-0043",
"clientName": "Haddad Trading",
"amount": "980.25",
"currency": "JOD",
"status": "draft",
"payload": {
"invoiceNumber": "INV-2026-0043"
},
"createdAt": "2026-10-04T13:58:21.000Z",
"lastEditedAt": "2026-10-04T13:58:21.000Z"
}
}
One saved document by its id. Scope: read.
curl https://xuvilo.com/api/v1/documents/c8e1f5a2-9b4d-4e7f-8a3c-2d6b0e1f9a57 \
-H "Authorization: Bearer $XUVILO_API_KEY"
Answer (200):
{
"data": {
"id": "c8e1f5a2-9b4d-4e7f-8a3c-2d6b0e1f9a57",
"type": "invoice",
"title": "INV-2026-0042",
"clientName": "Haddad Trading",
"amount": "1250.5",
"currency": "JOD",
"status": "sent",
"createdAt": "2026-10-03T08:00:00.000Z",
"lastEditedAt": "2026-10-03T08:20:15.000Z",
"payload": {
"invoiceNumber": "INV-2026-0042",
"items": [
{
"description": "Website maintenance",
"quantity": 1,
"unitPrice": 1250.5
}
]
}
}
}
Change some fields of a saved document. Fields you leave out keep their values. Scope: write.
curl -X PATCH https://xuvilo.com/api/v1/documents/c8e1f5a2-9b4d-4e7f-8a3c-2d6b0e1f9a57 \
-H "Authorization: Bearer $XUVILO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status":"paid"}'
Answer (200):
{
"data": {
"id": "c8e1f5a2-9b4d-4e7f-8a3c-2d6b0e1f9a57",
"type": "invoice",
"title": "INV-2026-0042",
"clientName": "Haddad Trading",
"amount": "1250.5",
"currency": "JOD",
"status": "paid",
"createdAt": "2026-10-03T08:00:00.000Z",
"lastEditedAt": "2026-10-04T14:05:00.000Z",
"payload": {
"invoiceNumber": "INV-2026-0042",
"items": [
{
"description": "Website maintenance",
"quantity": 1,
"unitPrice": 1250.5
}
]
}
}
}
Delete a saved document. This cannot be undone. Scope: write.
curl -X DELETE https://xuvilo.com/api/v1/documents/c8e1f5a2-9b4d-4e7f-8a3c-2d6b0e1f9a57 \
-H "Authorization: Bearer $XUVILO_API_KEY"
Answer (200):
{
"data": {
"id": "c8e1f5a2-9b4d-4e7f-8a3c-2d6b0e1f9a57",
"deleted": true
}
}
Documents shared from the app with a tracking link: where each one stands with the client (sent, seen, confirmed, paid or cancelled), when that happened, the share link, and the client's signature if they signed. v1 reads them and can cancel or reopen one. It cannot create them.
List the workspace's tracked documents, newest first, without their content. Scope: read.
limit: How many records to return, 1–100. Default 50.offset: How many records to skip from the start. Default 0.curl "https://xuvilo.com/api/v1/tracked-documents?limit=20" \
-H "Authorization: Bearer $XUVILO_API_KEY"
Answer (200):
{
"data": [
{
"id": "INV-7KQ2M9XPLR4T",
"docType": "invoice",
"docNumber": "INV-2026-0042",
"clientName": "Haddad Trading",
"amount": "1250.5",
"currency": "JOD",
"issueDate": "2026-10-03",
"dueDate": "2026-11-02",
"status": "confirmed",
"createdAt": "2026-10-03T08:21:02.000Z",
"seenAt": "2026-10-03T10:45:19.000Z",
"confirmedAt": "2026-10-03T10:52:40.000Z",
"paidAt": null,
"shareUrl": "https://xuvilo.com/invoice/track/INV-7KQ2M9XPLR4T",
"signature": {
"name": "Layla Haddad",
"signedAt": "2026-10-03T10:52:40.000Z"
}
}
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 1
}
}
One tracked document. Add include=payload to get the shared document's content as well, pictures included. Scope: read.
include: payload: also return the document's content.curl "https://xuvilo.com/api/v1/tracked-documents/INV-7KQ2M9XPLR4T?include=payload" \
-H "Authorization: Bearer $XUVILO_API_KEY"
Answer (200):
{
"data": {
"id": "INV-7KQ2M9XPLR4T",
"docType": "invoice",
"docNumber": "INV-2026-0042",
"clientName": "Haddad Trading",
"amount": "1250.5",
"currency": "JOD",
"issueDate": "2026-10-03",
"dueDate": "2026-11-02",
"status": "confirmed",
"createdAt": "2026-10-03T08:21:02.000Z",
"seenAt": "2026-10-03T10:45:19.000Z",
"confirmedAt": "2026-10-03T10:52:40.000Z",
"paidAt": null,
"shareUrl": "https://xuvilo.com/invoice/track/INV-7KQ2M9XPLR4T",
"signature": {
"name": "Layla Haddad",
"signedAt": "2026-10-03T10:52:40.000Z"
},
"payload": {
"invoiceNumber": "INV-2026-0042",
"items": [
{
"description": "Website maintenance",
"quantity": 1,
"unitPrice": 1250.5
}
]
}
}
}
Withdraw a shared document. Its link stays readable but stops taking a confirmation, a signature or a payment. A paid document cannot be cancelled (409 paid). Cancelling twice is harmless. Scope: write.
curl -X POST https://xuvilo.com/api/v1/tracked-documents/INV-7KQ2M9XPLR4T/cancel \
-H "Authorization: Bearer $XUVILO_API_KEY"
Answer (200):
{
"data": {
"id": "INV-7KQ2M9XPLR4T",
"docType": "invoice",
"docNumber": "INV-2026-0042",
"clientName": "Haddad Trading",
"amount": "1250.5",
"currency": "JOD",
"issueDate": "2026-10-03",
"dueDate": "2026-11-02",
"status": "cancelled",
"createdAt": "2026-10-03T08:21:02.000Z",
"seenAt": "2026-10-03T10:45:19.000Z",
"confirmedAt": "2026-10-03T10:52:40.000Z",
"paidAt": null,
"shareUrl": "https://xuvilo.com/invoice/track/INV-7KQ2M9XPLR4T",
"signature": {
"name": "Layla Haddad",
"signedAt": "2026-10-03T10:52:40.000Z"
}
}
}
Undo a cancel. The document goes back to where the client had taken it: confirmed, seen or sent. Scope: write.
curl -X POST https://xuvilo.com/api/v1/tracked-documents/INV-7KQ2M9XPLR4T/reopen \
-H "Authorization: Bearer $XUVILO_API_KEY"
Answer (200):
{
"data": {
"id": "INV-7KQ2M9XPLR4T",
"docType": "invoice",
"docNumber": "INV-2026-0042",
"clientName": "Haddad Trading",
"amount": "1250.5",
"currency": "JOD",
"issueDate": "2026-10-03",
"dueDate": "2026-11-02",
"status": "confirmed",
"createdAt": "2026-10-03T08:21:02.000Z",
"seenAt": "2026-10-03T10:45:19.000Z",
"confirmedAt": "2026-10-03T10:52:40.000Z",
"paidAt": null,
"shareUrl": "https://xuvilo.com/invoice/track/INV-7KQ2M9XPLR4T",
"signature": {
"name": "Layla Haddad",
"signedAt": "2026-10-03T10:52:40.000Z"
}
}
}
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A parameter or a field of the body is missing or not valid. The message names it, and so does field. |
| 401 | invalid_api_key | No key was sent, or the key is unknown or revoked. |
| 402 | plan_required | The workspace owner's plan does not include the API. Only once paid plans launch. |
| 403 | insufficient_scope | A read key tried to change something. Use a write key. |
| 403 | cors_not_allowed | The call came from a web browser: it carried an Origin header, as every call from a page on another site does. Call the API from your server instead. |
| 404 | not_found | There is no such record in this key's workspace. A record of another workspace gets the same answer. |
| 409 | paid | A paid tracked document cannot be cancelled. |
| 409 | product_limit | The workspace already keeps 2000 products, the most it may. Delete some to add more. |
| 413 | payload_too_large | The request body is larger than 1 MB. |
| 429 | rate_limited | More than 120 requests in a minute with this key, or more than 60 in a minute from one address with keys that do not work. Wait the number of seconds in the Retry-After header. |
| 500 | internal_error | Something failed on our side. Try again later; if it keeps happening, email [email protected]. |
| 503 | unavailable | Something the answer needs could not be reached for a moment: the request counter, or a tracked document's pictures. Wait the number of seconds in the Retry-After header, then try again. |
The version is part of the path: /api/v1. Within v1 we only add things: new endpoints, new optional parameters and new fields in answers. Write your code to ignore fields it does not know.
A change that could break an integration, such as removing or renaming a field or changing what it means, goes into a new version with notice. The Terms of Use cover the API, including how changes are announced.
A machine-readable description of v1 (OpenAPI 3.1) is at https://xuvilo.com/api/v1/openapi.json.
Use of the API is covered by the Terms of Use. Questions: [email protected].