/v1/openapiThe OpenAPI 3.1 description of this API, generated from the code that serves it
readcurl 'https://v1.uselinqo.com/v1/openapi'API reference
Read the catalog of services and professionals, check open slots and book appointments from your own software — with a key the account owner issues and revokes whenever they want.
The API is REST over JSON and answers at https://v1.uselinqo.com. Every key belongs to one company: everything it reads and writes stays inside that account, no exceptions.
This entire page — routes, fields, enums and error codes — is generated from GET /v1/openapi, the contract the server itself publishes, derived from the code that answers the calls. A new route or a new field shows up here without anyone rewriting anything, and it is the same document a client generator consumes.
const res = await fetch('https://v1.uselinqo.com/v1/openapi');
const spec = await res.json();That endpoint is public: no credential, no quota, served with Cache-Control: public, max-age=300. A description that required the very key it explains would be useless to anyone still deciding whether to integrate.
Do not send Authorization to this route. A GET with no non-standard header is a simple request, which the browser fires directly. Adding the header turns it into a preflight — which this route does not answer — and sends a credential to an endpoint that has no use for it.
The account administrator issues keys from the dashboard. At creation they pick a name, the scopes and, optionally, an expiry date.
SCOPE_MISSING.The key management screen is still being built in the dashboard. Until it ships, request yours at support@uselinqo.com.
Every route other than /v1/openapi expects the key in the Authorization header, as a Bearer token.
Authorization: Bearer bk_prod_<keyId>.<secret>The environment travels inside the token itself. A development key pasted into production fails while the token is being read, with a message that says so — not as an unexplained 401.
401 is the only response that does not use the error envelope. It comes from API Gateway, before the API code runs, and an HTTP API authorizer cannot reshape that body. Handle 401 by status, not by code. The exception is /v1/mcp, which authenticates on its own — see AI agents.
Each route requires at most one scope, and a key carries whichever ones the administrator picked. Grant the minimum the integration needs: a read-only key that leaks cannot book anything.
services:readprofessionals:readappointments:readappointments:writeclients:read3 routes require no scope:
GET /v1/openapi — requires no credential at all.GET /v1/meta — accepts any valid key.POST /v1/mcp — accepts any valid key.There are two per-minute buckets — one for reads, one for writes — and a monthly quota per key. Each route declares which bucket it falls into: that is the cost shown on its card below.
Agents do not have a bucket of their own. Through /v1/mcp, only tools/call spends, and it spends what the route behind the tool would — a read or a write; tools/list and server/discover are free. See AI agents.
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset: read those headers instead of counting calls on your side. Blowing the minute returns RATE_LIMITED and blowing the month returns QUOTA_EXCEEDED, both with status 429 — the first clears on its own, the second only at the turn of the month.
Every response also carries X-Request-Id. Keep the value in your logs: it is how support finds the call.
Straight from the API — the numbers in force right now
The surface a third party reaches with a key the company issued. Budgets, per key, on the Free plan: 120 reads/min, 20 writes/min, 1000 requests/month. They are the platform’s to set: the panel shows them and writes neither. The 401 is the one response that does not use the error envelope: it comes from API Gateway, which an HTTP API authorizer cannot reshape. A revoked key keeps working for up to 60 seconds: the authorizer caches its decision for that long. One route takes no credential at all: `GET /v1/openapi`, this document. A description that requires the key it explains is one nobody can read before deciding to integrate. The same surface is reachable by an agent at `POST /v1/mcp`, which speaks JSON-RPC rather than REST. It is one path here because every frame arrives as one request; the tools it offers are listed on that operation under `x-mcp-tools`. A key created while the panel is in the sandbox is a test key: `bk_prod_test_<keyId>.<secret>`. It reaches the company’s sandbox — a copy of its services, professionals and hours, with clients and appointments of its own — never notifies anyone, and spends no monthly quota. The per-minute limits are the same as a live key’s.
Listings accept limit and cursor and return data along with next_cursor. The cursor is opaque: pass the value back exactly as it came, without interpreting it.
let cursor = null;
do {
const url = new URL('https://v1.uselinqo.com/v1/services');
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.LINQO_API_KEY}` },
});
const page = await res.json();
handle(page.data);
cursor = page.next_cursor;
} while (cursor);Paginate until next_cursor comes back null, never until a page comes back short. The database cuts the read at 1 MB, so a page with few items can still carry a cursor — stopping there would silently leave records behind.
Booking an appointment requires the Idempotency-Key header. Without it the call is refused with IDEMPOTENCY_KEY_REQUIRED.
The reason is concrete: whoever hit a timeout cannot tell “my attempt went through” from “someone else took the slot”. With the key, repeating the request returns the earlier response instead of booking twice. The response is kept for 24 hours; the same key with a different body is a 400, IDEMPOTENCY_KEY_REUSED.
Generate a fresh value per booking attempt — a UUID does the job — and reuse it across every retry of that same attempt.
Listings accept fields with comma-separated names, to return only what your screen uses. A name the resource does not publish returns INVALID_FIELDS — the error is explicit on purpose, so that a mistyped field does not become a silently empty column.
curl 'https://v1.uselinqo.com/v1/services?fields=id,name,price' \
-H 'Authorization: Bearer $LINQO_API_KEY'Other than 401, every error arrives in the same envelope: { code, message }. Program against code, which is stable; message is for a human reading a log.
| Code | HTTP | Meaning |
|---|---|---|
IDEMPOTENCY_KEY_REQUIRED | 400 | A booking must carry `Idempotency-Key` |
IDEMPOTENCY_KEY_REUSED | 400 | That `Idempotency-Key` was used with a different body |
INVALID_CURSOR | 400 | `cursor` is not one we issued |
INVALID_FIELDS | 400 | `fields` named something this resource does not publish |
RANGE_TOO_WIDE | 400 | The date range exceeds what a single request may read |
VALIDATION_ERROR | 400 | The body or query does not match the schema |
SCOPE_MISSING | 403 | The key does not hold the scope this route requires |
NOT_FOUND | 404 | No such row in this company |
CLIENT_BUSY | 422 | The client is already booked at that time |
MODALITY_NOT_OFFERED | 422 | The professional offers no service in that modality |
SERVICE_MODALITY_MISMATCH | 422 | The professional does not offer one of the services in that modality |
SLOT_TAKEN | 422 | The professional is already booked at that time |
QUOTA_EXCEEDED | 429 | The monthly quota for this key is spent |
RATE_LIMITED | 429 | The per-minute budget for this key is spent |
An AI agent reaches the same /v1 surface through POST /v1/mcp, which speaks the Model Context Protocol — JSON-RPC over HTTP — instead of REST. A tool is only listed, and only callable, when the connection holds the scope its route requires.
Three steps in Claude Code. The account administrator needs to be the one approving in step 2.
claude mcp add --transport http linqo https://v1.uselinqo.com/v1/mcp/mcp, pick linqo and choose Authenticate. The browser opens on the linqo dashboard: sign in, review the scopes and approve./mcp now lists linqo as connected, with its tools. Ask the agent what it can do with linqo — it discovers the tools on its own; there is nothing to configure.Things to ask once it is connected
appointments:write.This is what the quick start uses. An MCP client with OAuth support needs only the URL — no key is copied anywhere. In clients configured by file, such as Cursor, declare just the URL; the client opens the browser the first time it connects:
{
"mcpServers": {
"linqo": {
"url": "https://v1.uselinqo.com/v1/mcp"
}
}
}What happens behind the scenes:
WWW-Authenticate pointing to the resource metadata (RFC 9728). From there the client discovers the authorization server (RFC 8414).lat_… access token is accepted by /v1/mcp alone; the REST routes answer 401 to it.S256 is accepted. Discovery lives at /.well-known/oauth-authorization-server.A client without OAuth — or a server-side integration — sends a regular key as a Bearer token, exactly as on the REST routes. Nothing changed for integrations that already use one. Keep the token in an environment variable and pass it as a header:
claude mcp add --transport http linqo \
https://v1.uselinqo.com/v1/mcp \
--header "Authorization: Bearer $LINQO_API_KEY"The same thing as configuration, for Claude Code and Cursor (Cursor ignores type):
{
"mcpServers": {
"linqo": {
"type": "http",
"url": "https://v1.uselinqo.com/v1/mcp",
"headers": { "Authorization": "Bearer bk_prod_<keyId>.<secret>" }
}
}
}Do not commit the token. Avoid --scope project in Claude Code: it writes to .mcp.json inside your repository, header and all. The default scope and --scope user write to ~/.claude.json, outside it. To keep the secret out of every file, reference an environment variable in the header value — ${LINQO_API_KEY} in Claude Code, ${env:LINQO_API_KEY} in Cursor.
The agent only sees the tools the key’s scopes reach — a tool outside them is not listed at all, rather than listed and refused. A read-only key makes an agent that answers questions about the calendar without changing it: a sensible start.
Unlike the rest of the API, a 401 here comes from the endpoint itself: it uses the error envelope (UNAUTHORIZED) and carries WWW-Authenticate. An invalid, expired or revoked token adds error="invalid_token", without saying which.
Quota is spent by tools/call only, at the cost of the route behind the tool. Listing tools (tools/list) and discovering the server (server/discover) are free.
12 tools, each driving one route. The route is the contract: follow the link to see exactly what a tool reads and writes.
| Tool | Route | Scope | Cost |
|---|---|---|---|
list_services | GET /v1/services | services:read | read |
get_service | GET /v1/services/{serviceId} | services:read | read |
list_service_professionals | GET /v1/services/{serviceId}/professionals | professionals:read | read |
list_professionals | GET /v1/professionals | professionals:read | read |
get_professional | GET /v1/professionals/{professionalId} | professionals:read | read |
check_availability | GET /v1/professionals/{professionalId}/availability | appointments:read | read |
list_clients | GET /v1/clients | clients:read | read |
get_client | GET /v1/clients/{clientId} | clients:read | read |
list_appointments | GET /v1/appointments | appointments:read | read |
get_appointment | GET /v1/appointments/{appointmentId} | appointments:read | read |
book_appointment | POST /v1/appointments | appointments:write | write |
update_appointment_status | PATCH /v1/appointments/{appointmentId} | appointments:write | write |
15 routes, grouped by resource. Each one states the scope it requires and which rate-limit bucket it falls into.
/v1/openapiThe OpenAPI 3.1 description of this API, generated from the code that serves it
readcurl 'https://v1.uselinqo.com/v1/openapi'/v1/metaDescribe this key: the routes it may call, its scopes and its limits
readcurl 'https://v1.uselinqo.com/v1/meta' \
-H 'Authorization: Bearer $LINQO_API_KEY'What this key reaches. No scope, and off the monthly quota.
| Field | Type | Description |
|---|---|---|
| stage | stringnullable | |
| key_id | string | |
| mode | string | `test` for a sandbox key: its own data, no notifications, no monthly quota.livetest |
| scopes | array<string> | |
| notifications | string | offon |
| limits | object | |
| routes | array<object> |
Cabeçalhos: X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
/v1/servicesList the services the company offers
services:readcost readcurl 'https://v1.uselinqo.com/v1/services' \
-H 'Authorization: Bearer $LINQO_API_KEY'| Parameter | Type | Description |
|---|---|---|
| limit | integer | Default: 50. |
| cursor | string | |
| fields | string | |
| modality | string | in_personvirtualat_home |
| Field | Type | Description |
|---|---|---|
| data* | array<Service> | |
| next_cursor* | stringnullable | Opaque. Page until it is null, never until a page comes back short: DynamoDB cuts a page at 1 MB read, so a short page can still carry a cursor. |
Cabeçalhos: X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
/v1/services/{serviceId}Read one service
services:readcost readcurl 'https://v1.uselinqo.com/v1/services/<serviceId>' \
-H 'Authorization: Bearer $LINQO_API_KEY'| Parameter | Type | Description |
|---|---|---|
| serviceId* | string |
| Parameter | Type | Description |
|---|---|---|
| fields | string |
A service the company offers.
| Field | Type | Description |
|---|---|---|
| id* | string | Service id. |
| name* | string | Service name, as the clinic set it. |
| duration* | integer | How long a visit of this service takes, in minutes. The end time of a booking is derived from it, which is why a request cannot send one. |
| price* | number | Price in the company currency. The price of a booking is the sum of its services, never a value the caller sends. |
| description* | stringnullable | Free text, or null. |
| active* | boolean | An inactive service stays readable and is not offered for new bookings. |
| created_at* | string (date-time) | When the row was created, UTC. |
| updated_at* | string (date-time) | When it last changed, UTC. |
Cabeçalhos: X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
/v1/services/{serviceId}/professionalsList the professionals who attend a service
professionals:readcost readcurl 'https://v1.uselinqo.com/v1/services/<serviceId>/professionals' \
-H 'Authorization: Bearer $LINQO_API_KEY'| Parameter | Type | Description |
|---|---|---|
| serviceId* | string |
| Parameter | Type | Description |
|---|---|---|
| fields | string |
| Field | Type | Description |
|---|---|---|
| data* | array<Professional> | |
| next_cursor* | stringnullable | Opaque. Page until it is null, never until a page comes back short: DynamoDB cuts a page at 1 MB read, so a short page can still carry a cursor. |
Cabeçalhos: X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
/v1/professionalsList the professionals on the roster
professionals:readcost readcurl 'https://v1.uselinqo.com/v1/professionals' \
-H 'Authorization: Bearer $LINQO_API_KEY'| Parameter | Type | Description |
|---|---|---|
| limit | integer | Default: 50. |
| cursor | string | |
| fields | string | |
| sort | string | Default: "created_at".created_atnext_available |
| service_id | string | |
| modality | string | in_personvirtualat_home |
| Field | Type | Description |
|---|---|---|
| data* | array<Professional> | |
| next_cursor* | stringnullable | Opaque. Page until it is null, never until a page comes back short: DynamoDB cuts a page at 1 MB read, so a short page can still carry a cursor. |
Cabeçalhos: X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
/v1/professionals/{professionalId}Read one professional
professionals:readcost readcurl 'https://v1.uselinqo.com/v1/professionals/<professionalId>' \
-H 'Authorization: Bearer $LINQO_API_KEY'| Parameter | Type | Description |
|---|---|---|
| professionalId* | string |
| Parameter | Type | Description |
|---|---|---|
| fields | string |
A professional. The CPF and the company id are not published.
| Field | Type | Description |
|---|---|---|
| id* | string | Professional id. |
| name* | string | Professional name. |
| email* | string (email)nullable | Contact e-mail, or null. |
| phone* | stringnullable | Contact phone in E.164 (e.g. +5511987654321), or null. |
| specialty* | stringnullable | Clinical specialty, or null. |
| council_registration* | stringnullable | Professional council registration, or null. |
| avatar_url* | stringnullable | Public picture URL, or null. |
| bio* | stringnullable | Free text, or null. |
| active* | boolean | An inactive professional stays readable and takes no new bookings. |
| timezone* | stringnullable | IANA timezone of this professional's calendar, or null when it follows the company's. |
| created_at* | string (date-time) | When the row was created, UTC. |
| updated_at* | string (date-time) | When it last changed, UTC. |
| next_available | objectnullable | Only with `sort=next_available`: the earliest free slot (minimum notice and booking window applied, 60 days at most), or null when there is none. |
Cabeçalhos: X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
/v1/professionals/{professionalId}/availabilityList the start times a visit of a given length could take on a date or in a range of at most 92 days
appointments:readcost readcurl 'https://v1.uselinqo.com/v1/professionals/<professionalId>/availability?date=2026-03-14&service_id=<service_id>' \
-H 'Authorization: Bearer $LINQO_API_KEY'| Parameter | Type | Description |
|---|---|---|
| professionalId* | string |
| Parameter | Type | Description |
|---|---|---|
| date | string | |
| from | string | |
| to | string | |
| service_id* | string | |
| tz | string | |
| modality | string | in_personvirtualat_home |
With `date`: one day, flat. With `from` and `to`: the same per day, under `days`.
Either AvailabilityDay
| Field | Type | Description |
|---|---|---|
| date* | string (date) | |
| timezone* | string | IANA timezone `slots` are read in: the professional’s, or the company’s. |
| duration_minutes* | integer | |
| slots* | array<string> | Free starts, `HH:mm`, wall clock in `timezone`. |
| starts_at* | array<string> | The same starts as ISO 8601 instants, in the offset of `tz` when sent, else of `timezone`. |
| Field | Type | Description |
|---|---|---|
| from* | string (date) | |
| to* | string (date) | |
| timezone* | string | |
| duration_minutes* | integer | |
| days* | array<object> | One per date from `from` to `to`, in order; a full day has empty `slots`. |
Cabeçalhos: X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
/v1/clientsList clients, redacted to identity and contact
clients:readcost readcurl 'https://v1.uselinqo.com/v1/clients' \
-H 'Authorization: Bearer $LINQO_API_KEY'| Parameter | Type | Description |
|---|---|---|
| limit | integer | Default: 50. |
| cursor | string | |
| fields | string |
| Field | Type | Description |
|---|---|---|
| data* | array<Client> | |
| next_cursor* | stringnullable | Opaque. Page until it is null, never until a page comes back short: DynamoDB cuts a page at 1 MB read, so a short page can still carry a cursor. |
Cabeçalhos: X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
/v1/clients/{clientId}Read one client, redacted to identity and contact
clients:readcost readcurl 'https://v1.uselinqo.com/v1/clients/<clientId>' \
-H 'Authorization: Bearer $LINQO_API_KEY'| Parameter | Type | Description |
|---|---|---|
| clientId* | string |
| Parameter | Type | Description |
|---|---|---|
| fields | string |
Identity and contact only. The CPF, birth date, address and the clinic’s notes stay in the panel.
| Field | Type | Description |
|---|---|---|
| id* | string | Client id. |
| name* | string | Client name. A client the API created from an inline booking is named "Reserva via API" until the clinic confirms it. |
| email* | string (email)nullable | Client e-mail, or null. |
| phone* | stringnullable | Client phone in E.164 (e.g. +5511987654321), or null. It is what an inline booking matches an existing client on. |
Cabeçalhos: X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
/v1/appointmentsList appointments in a date range of at most 92 days
appointments:readcost readcurl 'https://v1.uselinqo.com/v1/appointments?from=2026-03-14&to=2026-03-21' \
-H 'Authorization: Bearer $LINQO_API_KEY'| Parameter | Type | Description |
|---|---|---|
| limit | integer | Default: 50. |
| cursor | string | |
| fields | string | |
| from* | string | |
| to* | string | |
| professional_id | string | |
| service_id | string | |
| status | string | scheduledconfirmedcompletedcancelledno_show |
| Field | Type | Description |
|---|---|---|
| data* | array<Appointment> | |
| next_cursor* | stringnullable | Opaque. Page until it is null, never until a page comes back short: DynamoDB cuts a page at 1 MB read, so a short page can still carry a cursor. |
Cabeçalhos: X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
/v1/appointmentsBook an appointment; requires an Idempotency-Key header
appointments:writecost writecurl -X POST 'https://v1.uselinqo.com/v1/appointments' \
-H 'Authorization: Bearer $LINQO_API_KEY' \
-H 'Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000' \
-H 'Content-Type: application/json' \
-d '{
"professional_id": "<professional_id>",
"service_ids": [
"<service_ids>"
]
}'| Header | Type | Description |
|---|---|---|
| Idempotency-Key* | string | A caller whose request timed out cannot tell "my retry worked" from "someone else took the slot". The response is kept for 24 h; the same key with a different body is 400. |
| Field | Type | Description |
|---|---|---|
| professional_id* | string | |
| service_ids* | array<string> | |
| date | string | |
| start_time | string | |
| start_at | string (date-time) | |
| client_id | string | |
| client | object | |
| modality | string | in_personvirtualat_home |
| virtual_room | string | linqoexternal |
| notes | stringnullable | |
| address_zipcode | stringnullable | |
| address_street | stringnullable | |
| address_number | stringnullable | |
| address_complement | stringnullable | |
| address_neighborhood | stringnullable | |
| address_city | stringnullable | |
| address_state | stringnullable | |
| notify | boolean |
A booked visit.
| Field | Type | Description |
|---|---|---|
| id* | string | Appointment id. |
| professional_id* | string | Who attends the visit. |
| client_id* | string | Who the visit is for. |
| client_name* | string | The client name, denormalised so a listing needs no second read. |
| date* | string (date) | Day of the visit, `YYYY-MM-DD`. |
| start_time* | string | Start, `HH:mm`. |
| end_time* | string | End, `HH:mm`. Derived from the duration of the services, which is why a request cannot send it. |
| timezone* | string | IANA timezone `date`, `start_time` and `end_time` are read in: the professional’s, or the company’s. |
| start_at* | string (date-time)nullable | The start as an ISO 8601 instant with the timezone offset, e.g. `2026-09-16T10:00:00-03:00`. |
| end_at* | string (date-time)nullable | The end as an ISO 8601 instant with the timezone offset. |
| service_id* | stringnullable | The first service booked, or null. The full list has a route of its own. |
| service* | string | The services booked, as text, for display. |
| price* | numbernullable | The sum of the services booked. Never a value the caller sends. |
| payment_method* | stringnullable | How the visit was paid, or null.cashpixcreditdebittransferboleto |
| status* | string | Where the visit stands.scheduledconfirmedcompletedcancelledno_show |
| modality* | string | Where the visit happens.in_personvirtualat_home |
| virtual_room* | stringnullable | For a virtual visit: `linqo` when it happens in the linqo video room, whose link the backend writes into `virtual_link`; `external` for a meeting URL set in the panel. Null on any other modality.linqoexternal |
| virtual_link* | stringnullable | Meeting URL for a virtual visit, or null. With `virtual_room: linqo` it is the client’s link to the video room. Read-only here: it is rendered into the mail sent from the company identity, so a third party cannot set it. |
| address_zipcode* | stringnullable | Visit address, or null. |
| address_street* | stringnullable | Visit address, or null. |
| address_number* | stringnullable | Visit address, or null. |
| address_complement* | stringnullable | Visit address, or null. |
| address_neighborhood* | stringnullable | Visit address, or null. |
| address_city* | stringnullable | Visit address, or null. |
| address_state* | stringnullable | Visit address, or null. |
| notes* | stringnullable | Free text, or null. |
| confirmed_at* | string (date-time)nullable | When it was confirmed, UTC, or null. |
| cancelled_at* | string (date-time)nullable | When it was cancelled, UTC, or null. |
| cancellation_reason* | stringnullable | Why it was cancelled, or null. |
| created_at* | string (date-time) | When the row was created, UTC. |
| updated_at* | string (date-time) | When it last changed, UTC. |
Cabeçalhos: X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
/v1/appointments/{appointmentId}Read one appointment
appointments:readcost readcurl 'https://v1.uselinqo.com/v1/appointments/<appointmentId>' \
-H 'Authorization: Bearer $LINQO_API_KEY'| Parameter | Type | Description |
|---|---|---|
| appointmentId* | string |
| Parameter | Type | Description |
|---|---|---|
| fields | string |
A booked visit.
| Field | Type | Description |
|---|---|---|
| id* | string | Appointment id. |
| professional_id* | string | Who attends the visit. |
| client_id* | string | Who the visit is for. |
| client_name* | string | The client name, denormalised so a listing needs no second read. |
| date* | string (date) | Day of the visit, `YYYY-MM-DD`. |
| start_time* | string | Start, `HH:mm`. |
| end_time* | string | End, `HH:mm`. Derived from the duration of the services, which is why a request cannot send it. |
| timezone* | string | IANA timezone `date`, `start_time` and `end_time` are read in: the professional’s, or the company’s. |
| start_at* | string (date-time)nullable | The start as an ISO 8601 instant with the timezone offset, e.g. `2026-09-16T10:00:00-03:00`. |
| end_at* | string (date-time)nullable | The end as an ISO 8601 instant with the timezone offset. |
| service_id* | stringnullable | The first service booked, or null. The full list has a route of its own. |
| service* | string | The services booked, as text, for display. |
| price* | numbernullable | The sum of the services booked. Never a value the caller sends. |
| payment_method* | stringnullable | How the visit was paid, or null.cashpixcreditdebittransferboleto |
| status* | string | Where the visit stands.scheduledconfirmedcompletedcancelledno_show |
| modality* | string | Where the visit happens.in_personvirtualat_home |
| virtual_room* | stringnullable | For a virtual visit: `linqo` when it happens in the linqo video room, whose link the backend writes into `virtual_link`; `external` for a meeting URL set in the panel. Null on any other modality.linqoexternal |
| virtual_link* | stringnullable | Meeting URL for a virtual visit, or null. With `virtual_room: linqo` it is the client’s link to the video room. Read-only here: it is rendered into the mail sent from the company identity, so a third party cannot set it. |
| address_zipcode* | stringnullable | Visit address, or null. |
| address_street* | stringnullable | Visit address, or null. |
| address_number* | stringnullable | Visit address, or null. |
| address_complement* | stringnullable | Visit address, or null. |
| address_neighborhood* | stringnullable | Visit address, or null. |
| address_city* | stringnullable | Visit address, or null. |
| address_state* | stringnullable | Visit address, or null. |
| notes* | stringnullable | Free text, or null. |
| confirmed_at* | string (date-time)nullable | When it was confirmed, UTC, or null. |
| cancelled_at* | string (date-time)nullable | When it was cancelled, UTC, or null. |
| cancellation_reason* | stringnullable | Why it was cancelled, or null. |
| created_at* | string (date-time) | When the row was created, UTC. |
| updated_at* | string (date-time) | When it last changed, UTC. |
Cabeçalhos: X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
/v1/appointments/{appointmentId}Change an appointment status, including cancelling it
appointments:writecost writecurl -X PATCH 'https://v1.uselinqo.com/v1/appointments/<appointmentId>' \
-H 'Authorization: Bearer $LINQO_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"status": "scheduled"
}'| Parameter | Type | Description |
|---|---|---|
| appointmentId* | string |
| Field | Type | Description |
|---|---|---|
| status* | string | scheduledconfirmedcompletedcancelledno_show |
| cancellation_reason | stringnullable | |
| notify | boolean |
A booked visit.
| Field | Type | Description |
|---|---|---|
| id* | string | Appointment id. |
| professional_id* | string | Who attends the visit. |
| client_id* | string | Who the visit is for. |
| client_name* | string | The client name, denormalised so a listing needs no second read. |
| date* | string (date) | Day of the visit, `YYYY-MM-DD`. |
| start_time* | string | Start, `HH:mm`. |
| end_time* | string | End, `HH:mm`. Derived from the duration of the services, which is why a request cannot send it. |
| timezone* | string | IANA timezone `date`, `start_time` and `end_time` are read in: the professional’s, or the company’s. |
| start_at* | string (date-time)nullable | The start as an ISO 8601 instant with the timezone offset, e.g. `2026-09-16T10:00:00-03:00`. |
| end_at* | string (date-time)nullable | The end as an ISO 8601 instant with the timezone offset. |
| service_id* | stringnullable | The first service booked, or null. The full list has a route of its own. |
| service* | string | The services booked, as text, for display. |
| price* | numbernullable | The sum of the services booked. Never a value the caller sends. |
| payment_method* | stringnullable | How the visit was paid, or null.cashpixcreditdebittransferboleto |
| status* | string | Where the visit stands.scheduledconfirmedcompletedcancelledno_show |
| modality* | string | Where the visit happens.in_personvirtualat_home |
| virtual_room* | stringnullable | For a virtual visit: `linqo` when it happens in the linqo video room, whose link the backend writes into `virtual_link`; `external` for a meeting URL set in the panel. Null on any other modality.linqoexternal |
| virtual_link* | stringnullable | Meeting URL for a virtual visit, or null. With `virtual_room: linqo` it is the client’s link to the video room. Read-only here: it is rendered into the mail sent from the company identity, so a third party cannot set it. |
| address_zipcode* | stringnullable | Visit address, or null. |
| address_street* | stringnullable | Visit address, or null. |
| address_number* | stringnullable | Visit address, or null. |
| address_complement* | stringnullable | Visit address, or null. |
| address_neighborhood* | stringnullable | Visit address, or null. |
| address_city* | stringnullable | Visit address, or null. |
| address_state* | stringnullable | Visit address, or null. |
| notes* | stringnullable | Free text, or null. |
| confirmed_at* | string (date-time)nullable | When it was confirmed, UTC, or null. |
| cancelled_at* | string (date-time)nullable | When it was cancelled, UTC, or null. |
| cancellation_reason* | stringnullable | Why it was cancelled, or null. |
| created_at* | string (date-time) | When the row was created, UTC. |
| updated_at* | string (date-time) | When it last changed, UTC. |
Cabeçalhos: X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
/v1/mcpThe MCP endpoint: JSON-RPC over HTTP, one frame per request
curl -X POST 'https://v1.uselinqo.com/v1/mcp' \
-H 'Authorization: Bearer $LINQO_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"method": "<method>"
}'| Header | Type | Description |
|---|---|---|
| MCP-Protocol-Version | string | Required of a `2026-07-28` client, absent on a legacy one. When present it must match the version in `params._meta`.2026-07-282025-11-252025-06-182025-03-26 |
| Mcp-Method | string | The method named in the body. A disagreement is refused, in either era. |
| Field | Type | Description |
|---|---|---|
| jsonrpc* | string | 2.0 |
| id | stringnullable | |
| method* | string | `server/discover`, `tools/list` or `tools/call`; `initialize` and `ping` on a legacy client. |
| params | object |
A JSON-RPC 2.0 response: `result` on success, `error` otherwise, never both.
| Field | Type | Description |
|---|---|---|
| jsonrpc* | string | 2.0 |
| id | stringnullable | |
| result | object | |
| error | object |
The resources the routes return and accept. The descriptions come straight from the contract — the same text any integrator reads in /v1/openapi.
A service the company offers.
| Field | Type | Description |
|---|---|---|
| id* | string | Service id. |
| name* | string | Service name, as the clinic set it. |
| duration* | integer | How long a visit of this service takes, in minutes. The end time of a booking is derived from it, which is why a request cannot send one. |
| price* | number | Price in the company currency. The price of a booking is the sum of its services, never a value the caller sends. |
| description* | stringnullable | Free text, or null. |
| active* | boolean | An inactive service stays readable and is not offered for new bookings. |
| created_at* | string (date-time) | When the row was created, UTC. |
| updated_at* | string (date-time) | When it last changed, UTC. |
A professional. The CPF and the company id are not published.
| Field | Type | Description |
|---|---|---|
| id* | string | Professional id. |
| name* | string | Professional name. |
| email* | string (email)nullable | Contact e-mail, or null. |
| phone* | stringnullable | Contact phone in E.164 (e.g. +5511987654321), or null. |
| specialty* | stringnullable | Clinical specialty, or null. |
| council_registration* | stringnullable | Professional council registration, or null. |
| avatar_url* | stringnullable | Public picture URL, or null. |
| bio* | stringnullable | Free text, or null. |
| active* | boolean | An inactive professional stays readable and takes no new bookings. |
| timezone* | stringnullable | IANA timezone of this professional's calendar, or null when it follows the company's. |
| created_at* | string (date-time) | When the row was created, UTC. |
| updated_at* | string (date-time) | When it last changed, UTC. |
| next_available | objectnullable | Only with `sort=next_available`: the earliest free slot (minimum notice and booking window applied, 60 days at most), or null when there is none. |
Identity and contact only. The CPF, birth date, address and the clinic’s notes stay in the panel.
| Field | Type | Description |
|---|---|---|
| id* | string | Client id. |
| name* | string | Client name. A client the API created from an inline booking is named "Reserva via API" until the clinic confirms it. |
| email* | string (email)nullable | Client e-mail, or null. |
| phone* | stringnullable | Client phone in E.164 (e.g. +5511987654321), or null. It is what an inline booking matches an existing client on. |
A booked visit.
| Field | Type | Description |
|---|---|---|
| id* | string | Appointment id. |
| professional_id* | string | Who attends the visit. |
| client_id* | string | Who the visit is for. |
| client_name* | string | The client name, denormalised so a listing needs no second read. |
| date* | string (date) | Day of the visit, `YYYY-MM-DD`. |
| start_time* | string | Start, `HH:mm`. |
| end_time* | string | End, `HH:mm`. Derived from the duration of the services, which is why a request cannot send it. |
| timezone* | string | IANA timezone `date`, `start_time` and `end_time` are read in: the professional’s, or the company’s. |
| start_at* | string (date-time)nullable | The start as an ISO 8601 instant with the timezone offset, e.g. `2026-09-16T10:00:00-03:00`. |
| end_at* | string (date-time)nullable | The end as an ISO 8601 instant with the timezone offset. |
| service_id* | stringnullable | The first service booked, or null. The full list has a route of its own. |
| service* | string | The services booked, as text, for display. |
| price* | numbernullable | The sum of the services booked. Never a value the caller sends. |
| payment_method* | stringnullable | How the visit was paid, or null.cashpixcreditdebittransferboleto |
| status* | string | Where the visit stands.scheduledconfirmedcompletedcancelledno_show |
| modality* | string | Where the visit happens.in_personvirtualat_home |
| virtual_room* | stringnullable | For a virtual visit: `linqo` when it happens in the linqo video room, whose link the backend writes into `virtual_link`; `external` for a meeting URL set in the panel. Null on any other modality.linqoexternal |
| virtual_link* | stringnullable | Meeting URL for a virtual visit, or null. With `virtual_room: linqo` it is the client’s link to the video room. Read-only here: it is rendered into the mail sent from the company identity, so a third party cannot set it. |
| address_zipcode* | stringnullable | Visit address, or null. |
| address_street* | stringnullable | Visit address, or null. |
| address_number* | stringnullable | Visit address, or null. |
| address_complement* | stringnullable | Visit address, or null. |
| address_neighborhood* | stringnullable | Visit address, or null. |
| address_city* | stringnullable | Visit address, or null. |
| address_state* | stringnullable | Visit address, or null. |
| notes* | stringnullable | Free text, or null. |
| confirmed_at* | string (date-time)nullable | When it was confirmed, UTC, or null. |
| cancelled_at* | string (date-time)nullable | When it was cancelled, UTC, or null. |
| cancellation_reason* | stringnullable | Why it was cancelled, or null. |
| created_at* | string (date-time) | When the row was created, UTC. |
| updated_at* | string (date-time) | When it last changed, UTC. |
| Field | Type | Description |
|---|---|---|
| data* | array<Service> | |
| next_cursor* | stringnullable | Opaque. Page until it is null, never until a page comes back short: DynamoDB cuts a page at 1 MB read, so a short page can still carry a cursor. |
| Field | Type | Description |
|---|---|---|
| data* | array<Professional> | |
| next_cursor* | stringnullable | Opaque. Page until it is null, never until a page comes back short: DynamoDB cuts a page at 1 MB read, so a short page can still carry a cursor. |
| Field | Type | Description |
|---|---|---|
| data* | array<Client> | |
| next_cursor* | stringnullable | Opaque. Page until it is null, never until a page comes back short: DynamoDB cuts a page at 1 MB read, so a short page can still carry a cursor. |
| Field | Type | Description |
|---|---|---|
| data* | array<Appointment> | |
| next_cursor* | stringnullable | Opaque. Page until it is null, never until a page comes back short: DynamoDB cuts a page at 1 MB read, so a short page can still carry a cursor. |
With `date`: one day, flat. With `from` and `to`: the same per day, under `days`.
One of AvailabilityDay or AvailabilityRange.
| Field | Type | Description |
|---|---|---|
| date* | string (date) | |
| timezone* | string | IANA timezone `slots` are read in: the professional’s, or the company’s. |
| duration_minutes* | integer | |
| slots* | array<string> | Free starts, `HH:mm`, wall clock in `timezone`. |
| starts_at* | array<string> | The same starts as ISO 8601 instants, in the offset of `tz` when sent, else of `timezone`. |
| Field | Type | Description |
|---|---|---|
| from* | string (date) | |
| to* | string (date) | |
| timezone* | string | |
| duration_minutes* | integer | |
| days* | array<object> | One per date from `from` to `to`, in order; a full day has empty `slots`. |
What this key reaches. No scope, and off the monthly quota.
| Field | Type | Description |
|---|---|---|
| stage | stringnullable | |
| key_id | string | |
| mode | string | `test` for a sandbox key: its own data, no notifications, no monthly quota.livetest |
| scopes | array<string> | |
| notifications | string | offon |
| limits | object | |
| routes | array<object> |
A JSON-RPC 2.0 request. A frame with no `id` is a notification.
| Field | Type | Description |
|---|---|---|
| jsonrpc* | string | 2.0 |
| id | stringnullable | |
| method* | string | `server/discover`, `tools/list` or `tools/call`; `initialize` and `ping` on a legacy client. |
| params | object |
A JSON-RPC 2.0 response: `result` on success, `error` otherwise, never both.
| Field | Type | Description |
|---|---|---|
| jsonrpc* | string | 2.0 |
| id | stringnullable | |
| result | object | |
| error | object |
| Field | Type | Description |
|---|---|---|
| professional_id* | string | |
| service_ids* | array<string> | |
| date | string | |
| start_time | string | |
| start_at | string (date-time) | |
| client_id | string | |
| client | object | |
| modality | string | in_personvirtualat_home |
| virtual_room | string | linqoexternal |
| notes | stringnullable | |
| address_zipcode | stringnullable | |
| address_street | stringnullable | |
| address_number | stringnullable | |
| address_complement | stringnullable | |
| address_neighborhood | stringnullable | |
| address_city | stringnullable | |
| address_state | stringnullable | |
| notify | boolean |
| Field | Type | Description |
|---|---|---|
| status* | string | scheduledconfirmedcompletedcancelledno_show |
| cancellation_reason | stringnullable | |
| notify | boolean |
Tell us what you are building at support@uselinqo.com. The API grows from real usage, and knowing what is missing is what decides what ships next.