API & integrations (CRM, Make, n8n)
Connect your CRM or your ERP to Sattotal with API keys: synchronise customers and suppliers automatically and securely
The Sattotal API lets another program — your CRM, your ERP, Make, n8n, Zapier or your own script — read and create customers and suppliers in your organisation without anyone importing a CSV by hand. It is controlled with API keys that an administrator creates in Settings → API, each with its own name and with permissions per resource, and which can be revoked at any time. Here is what it does, how to create a key, how to call the API and how to set up a synchronisation with Make step by step.
Connect your CRM and your automations
Any tool that can make an HTTP request (Make, n8n, Zapier, your own CRM or ERP) can read, create and update customers and suppliers.
Named keys with permissions
One key per integration, and inside each key one permission per resource: you can grant read access to suppliers without granting write access to customers. Up to 10 active keys per organisation.
Incremental synchronisation
With the actualizadoDesde parameter you only fetch the customers or suppliers that have changed since the last run: ideal for a scenario that runs every few minutes.
Secure by design
The key is shown only once and stored as an irreversible hash. Each key only reaches its own organisation's data and can be revoked straight away.
Who can use it
Keys are created and revoked by the organisation's administrators. The API is included in the paid plans (Basic, Pro and Enterprise) and during the trial period; on the free plan the screen shows a padlock with the option to change plan. A key always acts on behalf of the organisation, not a person: it does not inherit any technician's permissions and does not appear as a team member.
How to create a key
Go to Settings → API
From the side menu, Settings, “API” card. You will see your organisation's keys (active and revoked) and a “How to connect” card with the base URL and an example.
Click “New key”
Give it a name that identifies the integration (“Shop CRM”, “Make”, “n8n”). If you ever have to revoke it, you will know which one it is.
Choose the permissions
Tick the resources the integration needs — Customers, Suppliers or both — and for each one choose “Read only” (look up) or “Read and write” (look up, create, update and archive). An unticked resource is a resource the key cannot reach. Choose the minimum it needs.
Copy the key and store it in your tool
The full key (it starts with sat_) is shown only once. Copy it with the button and paste it into Make, n8n or your CRM. If you lose it there is no way of recovering it: revoke it and create another.
Settings → API: key list and a newly created key
CRM Make
sat_Ab3k…x9Zq
n8n
sat_Qm7t…p2Lk
Key created
sat_Ab3k9Zq2Xv8Lp1Mn4Rt7Wy0Cd5Fg6Hj3Kl8Sx9ZqCopyThis is the only time you will see the full key. If you lose it, revoke it and create another.
Key permissions
Read only
Can list and look up that resource. Any attempt to create, change or archive receives a 403 error with code PERMISO_DENEGADO and the missing permission.
Read and write
Besides looking up, it can create, update and archive in that resource. It is the permission a two-way synchronisation needs.
Permissions are per resource
Permissions are per resource, and that has already shown: when suppliers were added, the keys that already existed — all of them for customers — did NOT gain access to them. They had to be ticked. The same will hold for any resource added later: an integration never sees more than you granted it.
How to authenticate
Send the key with every request, server to server, in the Authorization: Bearer sat_… header. If your tool does not allow authorisation headers, the x-api-key header is accepted too. The base URL is your Sattotal URL followed by /api/v1 (it is ready to copy on the Settings → API screen).
GET /api/v1/me Authorization: Bearer sat_Ab3k9…x9Zq
The API deliberately has no CORS: it is meant for servers and automation tools, not for web pages or apps running in your customers' browser. A key must never reach a browser.
What you can do (endpoints)
Every response has the form success + data (plus pagination on lists). You can ask for a customer by its id or by its code (CLI-0007); for a supplier, only by its id.
| Method | Path | What it does | Permission |
|---|---|---|---|
| GET | /me | Returns your organisation and the key you are calling with. Use it to “test the connection” in Make or n8n. | Any |
| GET | /clientes | Paginated list of customers. Filters: busqueda, tipo, activo and actualizadoDesde. Without activo it returns archived customers as well. | Read |
| POST | /clientes | Creates a customer with the same rules as the form (tax ID according to your country, no duplicate tax ID or email). | Write |
| GET | /clientes/{id} | Returns one customer by id or by code. | Read |
| PATCH | /clientes/{id} | Updates only the fields you send. PUT is accepted as a synonym. | Write |
| DELETE | /clientes/{id} | Archives the customer (activo = false). Nothing is deleted and it can be repeated without error. | Write |
| GET | /proveedores | Paginated list of suppliers. Filters: busqueda, activo, tipoProveedor, codigo and actualizadoDesde. Without activo it returns archived suppliers as well. | Read |
| POST | /proveedores | Creates a supplier. Only the name is required. If one already exists with the same tax ID or the same name, it returns 409 with the id of the existing one. | Write |
| GET | /proveedores/{id} | Returns one supplier by its id. The code does NOT work here: see the note below. | Read |
| PATCH | /proveedores/{id} | Updates only the fields you send. The contact list is replaced as a whole. PUT is accepted as a synonym. | Write |
| DELETE | /proveedores/{id} | Archives the supplier (activo = false). Nothing is deleted and it can be repeated without error. | Write |
| GET | /reparaciones | Paginated list of tickets, most recent booking-in first. Filters: cliente, estado, entradaDesde, entradaHasta and actualizadoDesde. | Read |
| GET | /reparaciones/{id} | Returns one ticket by its id or its ticket number. | Read |
| GET | /clientes/{id}/reparaciones | A customer's tickets (by the customer's id or code), with the same filters. | Read |
| GET | /clientes/{id}/resumen | A summary of the customer's activity: how many tickets they have (in total, open and by status), how much they have been quoted and when they last came in. | Read |
Customer fields
The fields are the same as in the customer record: nombre and tipo (particular or empresa) are required; nif_cif, apellidos, razonSocial, email, telefono, telefonoSecundario, direccion (calle, numero, piso, codigoPostal, localidad, provincia, pais) and notas are optional. The tax ID is stored in upper case and the email in lower case. In responses every key is always present, with null when there is no value, so the field mapping in your tool never breaks. Watch the name: nombre is the first name (or the trading name for a business) and the surname goes separately in apellidos, plural.
Fields the API does not recognise
If the body carries a key that does not exist (for example firstName instead of nombre, or the singular apellido instead of apellidos), the request is not rejected, but that value is not saved. So it does not go unnoticed, the response when creating or editing a customer includes avisos.camposIgnorados listing those keys; address keys carry their path, such as direccion.ciudad. If you see it, check your integration's field mapping. The fields the API itself returns (id, codigo, createdAt…) never trigger it, so you can read a customer, change it and send it back whole.
POST /api/v1/clientes
{ "firstName": "Daniel", "nombre": "Florea", "tipo": "particular" }
201 Created
{
"success": true,
"data": { "nombre": "Florea", "apellidos": null, … },
"avisos": { "camposIgnorados": ["firstName"] }
}Example: create a customer
Request
POST /api/v1/clientes
Authorization: Bearer sat_Ab3k9…x9Zq
Content-Type: application/json
{
"nombre": "Daniel",
"apellidos": "Florea",
"tipo": "empresa",
"razonSocial": "Assista Tech SRL",
"nif_cif": "RO12345678",
"codigoPersonalizado": "801",
"email": "daniel@example.com",
"telefono": "+40 700 000 000",
"direccion": { "localidad": "București", "pais": "RO" }
}Response (201)
{
"success": true,
"data": {
"id": "64b0…0001",
"codigo": "CLI-0042",
"codigoPersonalizado": "801",
"codigoVisible": "801",
"tipo": "empresa",
"nombre": "Daniel",
"apellidos": "Florea",
"razonSocial": "Assista Tech SRL",
"nif_cif": "RO12345678",
"email": "daniel@example.com",
"activo": true,
"createdAt": "2026-09-18T10:00:00.000Z",
"updatedAt": "2026-09-18T10:00:00.000Z"
}
}Suppliers
Besides customers, the API exposes the supplier catalogue: the same five endpoints, the same error codes and the same incremental synchronisation. It is only the catalogue: purchases and supplier invoices stay where they already are, in your ERP or in Sattotal.
Supplier fields
Only nombre is required. Optional: codigo, cif, email, telefono, telefonoSecundario, web, direccion (a single line, not an object as it is for customers), ciudad, provincia, codigoPostal, pais, contactos, tipoProveedor (general, producto, servicio, logistica or otro), formaPago, plazoPago (in days; 0 means payment on delivery), cuentaCliente and notas. Each contact carries nombre — required — plus cargo, telefono, email and notas; contacts have no identifier of their own.
How contacts are updated
The list you send REPLACES the one that was there. Send an empty list and every contact is deleted; leave the key out (or send null) and they stay as they were. That is what lets you read a supplier, change one field and send the whole thing back without odd side effects.
Request
POST /api/v1/proveedores
Authorization: Bearer sat_Ab3k9…x9Zq
Content-Type: application/json
{
"nombre": "Distribuciones Norte",
"cif": "B12345678",
"tipoProveedor": "producto",
"plazoPago": 30,
"cuentaCliente": "C-4471",
"contactos": [
{ "nombre": "Ana Ruiz", "cargo": "Ventas", "email": "ana@norte.es" }
]
}Response (201)
{
"success": true,
"data": {
"id": "64b0…0009",
"codigo": "PROV-004",
"nombre": "Distribuciones Norte",
"cif": "B12345678",
"tipoProveedor": "producto",
"plazoPago": 30,
"cuentaCliente": "C-4471",
"contactos": [
{ "nombre": "Ana Ruiz", "cargo": "Ventas",
"telefono": null, "email": "ana@norte.es", "notas": null }
],
"excluidoRecargoEquivalencia": false,
"activo": true,
"createdAt": "2026-09-19T08:00:00.000Z",
"updatedAt": "2026-09-19T08:00:00.000Z"
}
}Bank details are not exposed by the API
The supplier's IBAN and bank accounts are neither returned nor accepted, and that is deliberate: if a key ever leaked, it would be of no use for bank-change fraud, the most common kind with suppliers. cuentaCliente is returned, because that is YOUR customer number at that supplier and what an ERP uses to reconcile its purchases.
The equivalence surcharge is read-only
The excluidoRecargoEquivalencia field is returned and you may send it back with the same value (so you can return the whole object), but changing it through the API gives 400: it decides whether the “recargo de equivalencia” surcharge applies to your purchases from that supplier, so it moves the taxable base of your purchase invoices. It is changed from the supplier's record. Outside Spain it has no effect at all.
A supplier code does not identify a supplier
Unlike the customer one, the supplier code (PROV-004) is not unique: it can repeat even within your own shop. That is why GET /proveedores/{id} only accepts the id. If your ERP only stores the code, use the list filter: GET /proveedores?codigo=PROV-004 returns every match and you decide which one it is.
Repair tickets (read-only)
The API also lets you read repair tickets: ticket number, status, dates, the device (type, make, model, serial number, IMEI) and the quote. It is read-only: tickets are still created and moved between statuses in Sattotal, where the drop-off receipt and the collection are signed. The key needs the Repairs resource ticked; customer or supplier keys can't reach them.
Ticket fields
Each ticket includes numeroFicha, estado, prioridad, ubicacion, averiaDeclarada, diagnostico, the lifecycle dates (booking-in, diagnosis and repair start and end, customer notice, collection and ultimoCambioEstado, when it moved into its current status), a customer summary (id, codigo, codigoVisible, nombre, apellidos, razonSocial), the device (id, codigo, tipo, marca, modelo, numeroSerie, imei, color), the quote (numero, total, estado, fechaEnvio and fechaRespuesta, or null if there is none), the assigned technician (name only) and plazoEntregaEstimado, the turnaround time the customer was given on the booking-in receipt. Internal notes, signatures, photos, documents and device passwords are never returned.
Request
GET /api/v1/clientes/CLI-0042/reparaciones?estado=reparado Authorization: Bearer sat_Ab3k9…x9Zq
Response (201)
{
"success": true,
"data": [{
"id": "64b0…0042",
"numeroFicha": "ORD-2026-0042",
"estado": "reparado",
"prioridad": "normal",
"averiaDeclarada": "No carga",
"diagnostico": "Conector de carga dañado",
"fechas": {
"entrada": "2026-09-20T09:00:00.000Z",
"finReparacion": "2026-09-22T17:30:00.000Z",
"entrega": null
},
"cliente": { "id": "64b0…0001", "codigoVisible": "801", "nombre": "Daniel" },
"dispositivo": {
"tipo": "movil", "marca": "Apple", "modelo": "iPhone 13",
"numeroSerie": "F2LXX0000", "imei": "356789012345678"
},
"presupuesto": { "numero": "PRES-2026-0010", "total": 89.9, "estado": "aprobado" },
"tecnico": { "nombre": "Andrei Popescu" },
"plazoEntregaEstimado": "3 días laborables"
}],
"pagination": { "total": 1, "page": 1, "limit": 25, "totalPages": 1 }
}Find the customer from the number that is calling
If your CRM or phone system opens the customer record when a call comes in, use GET /clientes?telefono=<number>. It searches both the main and the secondary phone and ignores how the number is written: spaces, hyphens, your country's international code (+44 or 0044) and the leading 0. So “07700 900123” finds a customer saved as “+44 7700-900-123”. At least 6 digits are needed.
Customer summary for your CRM
GET /clientes/{id}/resumen returns, in a single call, what a CRM usually shows on the customer record: totalReparaciones, reparacionesAbiertas (the device is still in the workshop), porEstado, totalPresupuestado and totalPresupuestosAprobados (in the workshop's currency, which comes in moneda), primeraReparacion, ultimaReparacion and ultimaActividad (the latest change on any of their tickets). It needs the Repairs resource, just like the tickets.
Request
GET /api/v1/clientes?telefono=0722 123 456
→ { "data": [{ "id": "64b0…0001", "codigoVisible": "801", "telefono": "+40 722 123 456", … }] }
GET /api/v1/clientes/801/resumen
{
"success": true,
"data": {
"cliente": { "id": "64b0…0001", "codigoVisible": "801" },
"moneda": "RON",
"totalReparaciones": 7,
"reparacionesAbiertas": 1,
"porEstado": { "entregado": 6, "en_reparacion": 1 },
"totalPresupuestosAprobados": 1240.5,
"ultimaReparacion": "2026-10-02T09:15:00.000Z",
"ultimaActividad": "2026-10-06T16:40:00.000Z",
…
}
}Synchronise only what has changed
You do not need to fetch everything on every run. Store the date and time of the last run in your tool and request only the customers or suppliers modified since then with the actualizadoDesde parameter. The response is ordered by modification date, ascending, with a stable tie-breaker, so you can page through it without skipping any records. Archived records are included too (with activo = false), so your CRM can reflect the archiving.
GET /api/v1/clientes?actualizadoDesde=2026-09-18T10:00:00Z&limit=100&page=1 Authorization: Bearer sat_Ab3k9…x9Zq
First run
Step through GET /clientes with limit=100 and page=1, 2, 3… until totalPages runs out. Save the start time.
Subsequent runs
Request GET /clientes?actualizadoDesde=<saved time> and process only what comes back. Save this run's start time again.
Link, do not duplicate
Store the Sattotal id next to your CRM record. If creating returns 409 DUPLICADO, the response carries existenteId: link that one instead of creating another.
Pagination and search
Lists accept page (from 1) and limit (25 by default, 100 at most; anything higher is capped at 100). The busqueda field searches name, surname, company name, tax ID, email, code and telephone.
Error codes and what to do
Every error response carries success: false, a human-readable message and a stable code meant for programs to decide on. These are the ones you may receive:
NO_AUTORIZADOThe key is missing, has a format that is not ours or does not exist. Check the Authorization header.
CLAVE_REVOCADAThe key was valid but an administrator revoked it. Create a new one in Settings → API and update it in your tool.
PLAN_REQUERIDOThe organisation is on the free plan. The API works again after switching to a paid plan.
PERMISO_DENEGADOThe key lacks the permission for that operation. The response says in ambitoRequerido which one is missing: create a key with that permission.
NO_ENCONTRADOThere is no customer or supplier with that id in your organisation. Those of other organisations are never visible, and “does not exist” is not distinguished from “is not yours”.
DUPLICADOA record with that data already exists: tax ID or email for customers; tax ID, name or code for suppliers. The response carries campo and existenteId so you can link it instead of creating another.
VALIDACIONSome field fails validation (or the JSON is malformed). details lists the field and the reason, just like the form.
IDENTIFICADOR_FISCAL_REQUERIDOThat customer needs a tax ID: in your country it is mandatory for the customer type you sent (for example, always for companies).
RATE_LIMITToo many requests. Wait the seconds given in the Retry-After header, then retry.
API_DESACTIVADAThe API is temporarily disabled for maintenance. Try again later.
Limits
120 requests per minute per key (plenty for a periodic synchronisation; it stops a runaway loop). 10 active keys per organisation. Lists return at most 100 records per page. If the organisation is on the free plan with its monthly quota used up, creating customers is blocked just as in the app.
Step by step: synchronise customers with Make
A typical scenario: every 15 minutes, bring the customers and suppliers created or modified in Sattotal into your CRM. In n8n the equivalent uses an HTTP Request node and a Schedule node.
Create the key in Sattotal
Settings → API → New key, “Read only” if you will only read, “Read and write” if you will also create customers from the CRM. Copy the key.
HTTP module “Make a request”
URL: your base URL + /clientes. Method GET. Authorization header with value Bearer and your key. Tick “Parse response” to work with the JSON.
Test the connection
First of all, run one request to /me: if it returns your organisation and the key name, authentication is working.
Add the incremental filter
Store the last run date in a Data store or a variable and pass it as actualizadoDesde in the URL. Schedule the scenario every 15 minutes.
Map the fields into your CRM
Iterate data[] and map id, codigo, nombre, apellidos, email, telefono, nif_cif and direccion. Store the Sattotal id in your CRM so you update instead of duplicating.
To create customers from the CRM, add another HTTP module with method POST to /clientes and the customer's JSON body. If you receive a 409, use existenteId to link.
Security good practice
One key per integration
You can revoke one without breaking the others, and the list shows when each was last used.
Never in a browser or a public repository
Keep it in your tool's credential store (Make connections, n8n credentials, environment variables). If it ends up in a repository or on a website, consider it compromised.
If it leaks, revoke and create another
Revocation is immediate: the old key starts getting 401 CLAVE_REVOCADA. Update the new one in your tool and that is it.
API specification
The complete technical reference (paths, parameters, schemas and error codes) is published in an open format at /api/v1/openapi.json, with no key needed. By default it is in Spanish; add ?lang= with your language to get it translated (for example /api/v1/openapi.json?lang=en-GB, ?lang=en or ?lang=ro). You can import it into Postman, Insomnia, Make or n8n to have every call ready.
Frequently asked questions
I have lost the key, can I see it again?
No. It is shown only when created and then stored irreversibly. Revoke it in Settings → API and create another.
Are there webhooks so Sattotal notifies my CRM when something changes?
Not yet. The recommended way is to poll periodically with the actualizadoDesde parameter, which only returns what has changed.
What data does the API expose?
Customers and suppliers: list, create, look up, update and archive. Permissions are per resource, so a key created before suppliers existed does not reach them until an administrator ticks it. Plus repair tickets, read-only (status, dates, device and quote), with their own permission.
Can I delete a customer through the API?
No, only archive it (activo = false), the same as in the app. That applies to customers and suppliers alike. An archived record still appears in the synchronisation so your CRM can reflect it.
Is the API in the free plan?
No. It is included in Basic, Pro and Enterprise and during the trial period. On the free plan you can still see and revoke the keys you created.
Only the customer's surname comes through — why isn't the first name saved?
It is almost always the field mapping: the first name goes in nombre and the surname in apellidos. If your tool sends the first name under another key (firstName, name, apellido…), the API drops it and tells you so in avisos.camposIgnorados in the response. Fix the mapping and send the customer again with a PATCH.
Can I find the customer from the number that is calling me?
Yes. Request GET /api/v1/clientes?telefono= with the number exactly as you receive it: it does not matter whether it has an international code, spaces or hyphens, and the secondary phone is searched too. With their id you can then request their tickets (/clientes/{id}/reparaciones) and their summary (/clientes/{id}/resumen).
