Guides
Visits
Create visits (every field, priorities, appointments, address and patient), read and list them with filters and pagination, get the estimated arrival time, cancel them and download the care report.
Last reviewed: October 1, 2026
A visit is a request for a healthcare professional to see a patient at home. This guide covers everything you can do with visits; the details of every field are also in the API reference.
| Method | Path | Scope | Purpose |
|---|---|---|---|
POST | /visits | visits:write | Create a visit |
GET | /visits/{id} | visits:read | Retrieve a visit |
GET | /visits | visits:read | List visits |
GET | /visits/{id}/eta | visits:read | Estimated arrival time |
POST | /visits/{id}/cancel | visits:write | Cancel |
GET | /visits/{id}/report | reports:read | Care report |
Create a visit
POST /visits with the patient, the address, the service type and the reason. Always send a unique Idempotency-Key header for that request: if the connection drops and you repeat it, no duplicate visit is created (idempotency).
curl -X POST "$SERAD_API_URL/visits" \
-H "X-Api-Key: $SERAD_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: crm-exp-2026-004512-1" \
-d '{
"patient": {
"firstName": "María",
"lastName": "García López",
"documentType": "DNI",
"documentNumber": "12345678Z",
"birthDate": "1948-05-17",
"sex": "FEMALE",
"phone": "+34600111222"
},
"address": {
"line1": "Calle de Alcalá 120",
"line2": "3º B",
"postalCode": "28009",
"city": "Madrid",
"province": "Madrid",
"accessNotes": "Portal 2, código 1234#. Ascensor al fondo."
},
"serviceTypeCode": "GENERAL_CONSULTATION",
"priority": "URGENT",
"reason": "Fiebre alta desde ayer, 39 ºC, malestar general",
"externalRef": "EXP-2026-004512",
"callerName": "Ana García (hija)",
"callerPhone": "+34600111333"
}'The response is 201 Created with the full visit (see the visit object). If you repeat the same request with the same Idempotency-Key, you get 200 OK with the same visit and the Idempotent-Replayed: true header.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
patient | object | Yes, unless you send patientId | Patient details. See the patient. |
patientId | uuid | No | Patient from one of your earlier visits. If present, patient is not needed. |
address | object | Yes | Address of the visit. See the address. |
serviceTypeCode | string | Yes, or serviceTypeId | Service type code, from the catalogue. Recommended: it's the same in every environment. |
serviceTypeId | uuid | Yes, or serviceTypeCode | Service type id. It differs between environments; if both are sent, the code wins. |
priority | URGENT | NORMAL | SCHEDULED | No | Defaults to NORMAL, or SCHEDULED if you send scheduledFrom. See priority and appointments. |
scheduledFrom | ISO 8601 date-time | Appointments only | Start of the appointment, with a time zone (2026-10-02T09:00:00+02:00). |
estimatedMinutes | integer | No | Estimated duration: from 30 to 240, in steps of 10. Defaults to 60. |
reason | string (3–2000) | Yes | Reason for the request, as the patient or the referring doctor would describe it. |
externalRef | string (≤ 100) | If your contract requires it | Your case number. See catalogue. |
insurerId | uuid | Intermediaries only | End insurer, from catalog.insurers. |
callerName | string (≤ 200) | No | Who is requesting the visit, if not the patient ("Ana García (hija)", i.e. the patient's daughter). |
callerPhone | string | No | Contact phone number of the requester. Same format as patient.phone. |
scheduledTo | date-time | No | Deprecated: use estimatedMinutes. If it's sent without it, the slot duration is rounded to the 10-minute step. |
Request headers:
| Header | Required | Description |
|---|---|---|
Idempotency-Key | Recommended | Unique identifier of the operation in your system (≤ 200 characters). |
X-Sandbox-Simulation | No | Sandbox only: manual so that the visit doesn't move forward on its own. |
Priority and appointments
priority | When to use it | What happens |
|---|---|---|
URGENT | Non-life-threatening urgent case: the patient needs care as soon as possible (high fever, severe pain, a minor fall). | It's assigned with priority to the first suitable unit available. |
NORMAL | Needs to be seen today, with no specific time. | It's assigned according to the units' workload and routes. |
SCHEDULED | Appointment at a specific time (wound care, check-ups, follow-ups). | The unit arrives within the appointment slot. |
For an appointment, send scheduledFrom (start) and estimatedMinutes (how long the care will take). We work out the end of the appointment: scheduledTo = scheduledFrom + estimatedMinutes.
{
"priority": "SCHEDULED",
"scheduledFrom": "2026-10-02T09:00:00+02:00",
"estimatedMinutes": 40
}Appointment rules:
scheduledFromalways includes a time zone (+02:00,Z). Without one, the server would interpret it as UTC.- It can't start more than 15 minutes in the past (
/errors/visits/schedule-in-past): the small delay between the patient's call and the request is allowed, but scheduling past appointments is not. - It can't be more than 90 days ahead (
/errors/visits/schedule-beyond-horizon). estimatedMinutesgoes from 30 to 240 in steps of 10 (30, 40, 50… 240). Any other value returns a validation400. If you don't send it, 60 minutes is assumed.
The dispatch centre can extend the estimated time while the visit is in progress; the updated value is returned in estimatedMinutes when you retrieve the visit.
The address
| Field | Type | Required | Description |
|---|---|---|---|
line1 | string (1–255) | Yes | Street and number: Calle de Alcalá 120. |
line2 | string (≤ 255) | No | Floor, door, staircase: 3º B. |
postalCode | string (4–10) | Yes | Postcode: 28009. |
city | string (1–100) | Yes | Municipality: Madrid. |
province | string (≤ 100) | No | Province. |
accessNotes | string (≤ 1000) | No | Directions: building entrance, door entry code, whether to call ahead, pets… Only the unit sees them. |
latitude / longitude | number | No | Coordinates of the address, if you have them. |
If you don't send coordinates, we geocode the address. If geocoding fails or the postcode doesn't match the street, the visit is still created and the dispatch centre reviews the address before assigning it, which may delay the unit's departure. To avoid this:
- Keep the fields clean: the street number in
line1, the floor inline2. - Send the correct postcode (it's the most common mistake).
- If your system already has validated coordinates for the address, send them.
Coverage is Madrid and the Community of Madrid. If you're not sure whether an address is covered, ask us.
The patient
| Field | Type | Required | Description |
|---|---|---|---|
firstName | string (1–100) | Yes | First name. |
lastName | string (1–150) | Yes | Surnames. |
documentType | DNI | NIE | PASSPORT | NONE | No | Defaults to NONE (minors without an ID card, or patients who don't provide one). |
documentNumber | string (≤ 30) | Yes, except with NONE | Document number. DNI and NIE numbers are validated (format and check letter). Not allowed with NONE. |
birthDate | date YYYY-MM-DD | Recommended | Date of birth: not in the future and less than 120 years ago. |
sex | MALE | FEMALE | OTHER | UNKNOWN | No | Registered sex (it's clinically relevant). |
phone | string | Recommended | The patient's phone number, with or without the country code: +34600111222. The unit calls this number if it can't find the address. |
How we identify the patient. Using the document type and number and the date of birth, we check whether the patient already exists in Salud en Ruta; if so, the visit is linked to them (so the professional has their history); if not, a new record is created. Without a document (NONE) there's no reliable way to tell whether it's the same person, so a new record is created every time. That's why we ask you to always send the document and date of birth when you have them. More details, and which data we return, in patients and privacy.
Patients from earlier visits. If a patient has already had a visit from you, you can send their patientId instead of patient. We only accept patients from your visits: a patientId that isn't returns a 404 (/errors/patients/not-found), just like one that doesn't exist. In practice, though, the simplest approach over the API is to send the patient's details again: the result is the same.
Common errors when creating a visit
| Status | type | Cause |
|---|---|---|
400 | /errors/validation-error | A field has the wrong format. errors[] tells you which one (patient.documentNumber, estimatedMinutes…). |
400 | /errors/provider-api/service-type-required | You sent neither serviceTypeCode nor serviceTypeId. |
400 | /errors/provider/service-type-not-allowed | That service type isn't in your catalogue. |
400 | /errors/visits/external-ref-required | Your contract requires externalRef. |
400 | /errors/provider/insurer-not-managed | insurerId isn't one of your insurers (or you aren't an intermediary). |
400 | /errors/patients/document-invalid | The DNI or NIE isn't valid (format or check letter). |
400 | /errors/visits/schedule-in-past · /errors/visits/schedule-beyond-horizon | The appointment is in the past or more than 90 days ahead. |
409 | /errors/provider-api/idempotency-key-reused | You reused an Idempotency-Key with a different body. |
Every code is listed in errors.
The visit object
Every operation on a visit returns the same structure (inside data):
{
"id": "c6a3c348-40b6-4158-a44a-7d85f01a0201",
"code": "AV-000123",
"status": "EN_ROUTE",
"statusLabel": "En camino",
"priority": "URGENT",
"origin": "PROVIDER_API",
"externalRef": "EXP-2026-004512",
"serviceType": { "id": "57605fe9-bade-4040-b403-5eeb58e57c9e", "code": "GENERAL_CONSULTATION", "name": "Consulta general" },
"requiredProfessional": "DOCTOR",
"insurer": null,
"patientName": "María García López",
"address": { "line1": "Calle de Alcalá 120", "city": "Madrid", "postalCode": "28009" },
"serviceDate": "2026-10-01",
"scheduledFrom": null,
"scheduledTo": null,
"estimatedMinutes": 60,
"milestones": {
"requestedAt": "2026-10-01T09:10:00.000Z",
"assignedAt": "2026-10-01T09:15:00.000Z",
"departedAt": "2026-10-01T09:32:00.000Z",
"arrivedAt": null,
"finishedAt": null,
"cancelledAt": null,
"invoicedAt": null
},
"unit": { "type": "MEDICAL", "label": "Unidad médica" },
"outcome": null,
"cancellationReason": null,
"cancelledWithCharge": false,
"cancellable": false,
"reportAvailable": false,
"invoiceId": null,
"createdAt": "2026-10-01T09:10:00.000Z",
"updatedAt": "2026-10-01T09:32:00.000Z"
}| Field | Description |
|---|---|
id | Visit identifier. Use it in every other call. It differs between environments. |
code | Human-readable code (AV-000123): the one the dispatch centre will quote on the phone. |
status / statusLabel | Status (lifecycle) and its label in the language of the request. |
origin | How it came in: PROVIDER_API, PROVIDER_PORTAL or INTRANET (by phone). |
serviceDate | Day of the service (YYYY-MM-DD, Madrid time). |
milestones | Time of each milestone; null if it hasn't happened yet. It's the reliable source for what has happened. |
unit | Only the unit type (MEDICAL/NURSING), from assignment onwards. Never names, licence plates or location. |
outcome | Result once finished: ATTENDED, PATIENT_ABSENT (not at home), REFERRED_HOSPITAL (referred to hospital), REFUSED (refused care) or CANCELLED. |
cancellable | Whether you can cancel it through the API right now. |
cancelledWithCharge | Whether the cancellation is chargeable (only those made by the dispatch centre with the unit already on its way, as per your contract). |
reportAvailable | Whether there's a downloadable report. |
invoiceId | The invoice it's included in, once invoiced. |
Retrieve a visit
GET /visits/{id} returns the visit with its timeline (timeline): every status change with its time.
curl "$SERAD_API_URL/visits/c6a3c348-40b6-4158-a44a-7d85f01a0201" \
-H "X-Api-Key: $SERAD_API_KEY"{
"timeline": [
{ "status": "PENDING", "label": "Aviso recibido", "at": "2026-10-01T09:10:00.000Z" },
{ "status": "ASSIGNED", "label": "Unidad asignada", "at": "2026-10-01T09:15:00.000Z" },
{ "status": "EN_ROUTE", "label": "Unidad en camino", "at": "2026-10-01T09:32:00.000Z" }
]
}A visit belonging to another provider returns 404, just like one that doesn't exist: we don't confirm the existence of visits that aren't yours.
List visits
GET /visits returns your visits, newest first, paginated.
curl "$SERAD_API_URL/visits?status=PENDING,ASSIGNED,EN_ROUTE&dateFrom=2026-10-01&page=1&limit=50" \
-H "X-Api-Key: $SERAD_API_KEY"| Parameter | Description |
|---|---|
page | Page number, starting at 1. Defaults to 1. |
limit | Visits per page, from 1 to 100. Defaults to 10. |
status | One or more statuses, comma-separated or by repeating the parameter: status=PENDING,ASSIGNED. |
priority | URGENT, NORMAL or SCHEDULED. |
dateFrom / dateTo | Service day from / to (inclusive), YYYY-MM-DD. They can't be in the future, and dateTo can't be earlier than dateFrom. |
externalRef | Your exact case number. It's how you find a visit without storing our id. |
serviceTypeId | Service type. |
search | Text matched against the visit code or the case number. |
sortOrder | DESC (default, newest first) or ASC, by request time. |
{
"status": 200,
"data": {
"items": [ { "id": "c6a3c348-…", "code": "AV-000123", "status": "EN_ROUTE", "…": "…" } ],
"pagination": { "page": 1, "limit": 50, "totalItems": 134, "totalPages": 3 }
},
"message": "Operación realizada correctamente"
}To go through them all, request pages until page = totalPages. If your goal is to keep your system in sync, use webhooks and keep the list for periodic reconciliation (for example, the day's visits every night).
Estimated arrival time
While the unit is en route (EN_ROUTE), GET /visits/{id}/eta works out how long it will take to arrive by road from its last known position:
curl "$SERAD_API_URL/visits/c6a3c348-40b6-4158-a44a-7d85f01a0201/eta" \
-H "X-Api-Key: $SERAD_API_KEY"{
"status": 200,
"data": {
"status": "EN_ROUTE",
"etaMinutes": 14,
"estimatedArrivalAt": "2026-10-01T09:46:00.000Z",
"estimated": false,
"calculatedAt": "2026-10-01T09:32:10.000Z"
},
"message": "Operación realizada correctamente"
}- In any other status, or if there's no recent position for the unit,
etaMinutesandestimatedArrivalAtarenull. estimated: truemeans the road route couldn't be calculated and the figure is a straight-line approximation: allow a wider margin.- We never return the unit's position, only the time.
- The ETA changes with traffic. If you show it to the patient, refresh it every few minutes, not every second (it counts towards your rate limit).
Cancel a visit
POST /visits/{id}/cancel with the reason:
curl -X POST "$SERAD_API_URL/visits/c6a3c348-40b6-4158-a44a-7d85f01a0201/cancel" \
-H "X-Api-Key: $SERAD_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "El paciente ya no necesita la visita" }'| Field | Type | Required | Description |
|---|---|---|---|
reason | string (3–1000) | Yes | Reason for the cancellation. The dispatch centre sees it. |
Rules:
- You can only cancel through the API while the visit is
PENDINGorASSIGNED, and then it's free of charge. Thecancellablefield in the response tells you whether it's possible right now. - With the unit en route or on site, the API responds
409with the type/errors/provider/visit-not-cancellable. In that case, call the dispatch centre (+34 611 673 280): the unit has already left and needs to be told. Depending on your contract, that cancellation may be chargeable (cancelledWithCharge). - A visit that is already finished or cancelled can't be cancelled either (same
409). - When you cancel, you receive the
visit.cancelledwebhook.
Care report
Once the professional signs the visit report, you can download it as a PDF with GET /visits/{id}/report (scope reports:read), only if:
- the report is signed,
- Salud en Ruta has shared it with your organisation, and
- the patient has consented to it being sent to you.
curl "$SERAD_API_URL/visits/c6a3c348-40b6-4158-a44a-7d85f01a0201/report" \
-H "X-Api-Key: $SERAD_API_KEY" \
-o informe-AV-000123.pdf- The response is the PDF (
Content-Type: application/pdf,Content-Disposition: attachment; filename="informe-AV-000123.pdf"), not JSON. - If any of the three conditions isn't met, it responds
404with the type/errors/provider/report-not-available. Without the patient's consent there's no report, and we can't bypass that rule. - You'll know it's available from the
report.sharedwebhook or fromreportAvailable: truewhen you retrieve the visit. - Every download is logged as an access to clinical data, together with the key that made it. Download it once, store it in your document system with the safeguards the GDPR requires for health data, and don't request it again every time someone looks at it.
More on health data in patients and privacy.
Something missing or not matching what you see? Email our technical team: info@saludenruta.es.
