Developers

The Meevia API

Read the schedule, book appointments and look up the catalog and the patient record from your own system. This page takes you from your first key to your first successful response.

On this page

What it is and who it's for

The API opens up, to your system, the parts of the clinic that make sense to automate: the schedule, the catalog of professionals and procedures, and the patient's basic record. Whatever you do through it follows the same rules that apply to the people running the clinic inside the platform.

It's HTTP with JSON, with no required library and no SDK to install. If your environment knows how to make a request and read a response, it already knows how to talk to Meevia.

Common cases

  • A website or app of your own where the patient picks a time and books without help.
  • A system the clinic already uses that needs to see the day's schedule without anyone typing it twice.
  • Internal automation: a dashboard of your own, a report, a routine that confirms attendance.

What the clinic needs to have

API access is part of the clinic's plan. If the current plan doesn't include it, the very first call comes back refused, and no change to your code will fix that: it's a conversation with the sales team.

What isn't here yet

This version reads and writes the schedule, and reads the catalog and the patient. Meevia telling your system when something happens at the clinic, along with billing and payments, is not part of it.

How to get a key

A key is born in the developer panel, which lives at developers.meevia.app and has a shell of its own, outside the clinic's system. It works that way because the person building the integration often doesn't belong to the clinic. There is no path through the API for creating a key. The panel's interface is in Brazilian Portuguese, so the tab and role names quoted below are exactly the ones you will read on screen.

Open the developer panel

  1. 1Open the developer panelThe address is developers.meevia.app. Anyone already working inside the clinic's system reaches the same place through Integrações (Integrations), in the API e Webhooks card, which is a door to the panel.
  2. 2Sign in with your account, or ask for an invitationWhoever administers the clinic signs in straight away. Anyone from outside has to be invited: in the Acessos (Access) tab, whoever administers the panel invites by e-mail and chooses between Somente ver (view only), which can look but not act, and Pode administrar (can administer), which also issues keys. Asking for the invitation beats asking for a ready-made key, because a key that travels by message has already leaked.
  3. 3Pick the clinicThe panel serves every clinic you have access to, and the one picked at the top owns everything the tabs show. If you work for several, this is where you switch.
  4. 4In the Chaves (Keys) tab, create a new keyGive it a name that says where it will be used. Six months from now, that name is the only way to revoke the right key without taking down the wrong integration.
  5. 5Pick the mode and the scopesThe mode decides whether the key touches the real clinic or the test environment. The scopes decide what it can do. Tick only what the integration needs.
  6. 6Copy the key before closing the windowThe full key appears exactly once, at the moment it is issued. Meevia keeps the piece that identifies it and never the secret part, so closing without copying has no recovery: revoke the key and issue another.

One key, one clinic

A key is good for the clinic that issued it, and for that one only. If you serve several clinics, that means several keys, each stored separately and replaceable without touching the others. No key can see more than one clinic.

Where to keep it

Keep the key on the server, in your environment's variable vault. A key sitting inside the code of an app or a web page is a published key, because anybody can read it.

When to revoke

Revoking takes effect at once and cannot be undone: the key stops working and its calls start coming back refused. Do it when you change suppliers, when you shut an integration down, and at the faintest suspicion of a leak.

Who can do what

The Pode administrar role in the panel opens both of the things that change something: issuing and revoking keys, and resetting the test environment. It comes by invitation and applies per clinic, so someone from outside does both without depending on anybody. Resetting has a second path: whoever administers the clinic can also reset. Someone holding only Somente ver sees the keys and issues none; the panel says so on screen instead of hiding the button.

Authentication

Every call goes out authenticated. The key travels in the authorization header, in the scheme the example shows, and that's the whole of it: no login, no session, no token that expires halfway through.

Requestbash
curl -X GET \
  -H "Authorization: Bearer mv_live_a1b2c3d49f8e7d6c5b4a39281706f5e4d3c2b1a0" \
  "https://wnricobmssysbwfhbmsm.supabase.co/functions/v1/api-v1/v1/practitioners"

The key is a single value, and it is that whole value that travels in the header. It has three pieces glued together: the prefix says which mode the key is in, the middle piece identifies it and is what shows up in the panel's Chaves tab, and the last one is the secret part. Meevia never stores that last piece in the clear, so not even support can read yours. Always send the complete key: sending only the secret part is the slip that produces the most authorization refusals.

A key that is missing, mistyped, revoked or expired all get the same refusal, without saying which of the four it was. That is deliberate. Saying which one would hand a clue to whoever is guessing.

Two modes

Production

mv_live_

Touches the real clinic. An appointment created from here shows up on the front desk's schedule and sets off whatever the clinic has set up to go off.

Test

mv_test_

Touches a separate environment, with made-up data, that exists only for you to experiment in. Nothing that happens there comes anywhere near the real clinic.

The mode is decided when the key is issued and does not change afterwards. In practice you keep both keys and swap the environment variable to flip from one side to the other.

The key in the examples is made up and works nowhere: it is there only so you can recognize the shape of yours. Notice that its prefix is a production one. If you follow the walkthrough in test mode, as this page recommends, yours will start differently.

Scopes

Each scope opens one part of the API, and a key only does what its scopes allow. A missing scope shows up on the first call that needs it, not when the key is issued.

appointments:read

Read the schedule

List the appointments in a date range and open one of them.

appointments:write

Change the schedule

Create an appointment, change the status of one, and cancel.

catalog:read

Read the catalog

Professionals, procedures and a given day's free time slots.

patients:read

Read a patient

Open the basic record of a patient you already know by identifier. There is no open search by name in this version.

Grant the minimum. An integration that only shows the schedule on a wall display has no business booking appointments. And a lean key is a small amount of damage on the day it leaks.

Scopes cannot be edited once the key is issued. Widening them means issuing a new key with what was missing, swapping it in your environment and revoking the old one.

Test environment

The clinic gets a test environment of its very own, isolated from the real clinic. Issuing the first test key already creates it, so there is nothing to provision and nothing to request.

What's already inside

There's one sample patient, one sample procedure and one sample professional. None of them exists in the real clinic. The professional and procedure lists return the identifiers of those two, so looking up the catalog and free time slots works in your very first sitting. Registering people and services belongs to the clinic: through the API, what you create is an appointment.

What you can't test here

The API writes in three places: creating an appointment, changing its status and cancelling it. None of the three can be exercised here today, and the reason comes in a chain. Creating needs the patient's identifier, which appears neither on screen nor in the result of any query, because there is no patient listing. Changing the status and cancelling need an appointment's identifier, and it would only exist if creating had worked: the appointment list starts out empty here. Anyone who needs to test a write does it in the real clinic, with the care that deserves: book into an empty slot and cancel as soon as you have confirmed it worked.

How to reset it

The panel's Ambiente de teste (Test environment) tab has a button that throws away the current environment and hands you another, clean one, with the same starting data. Test keys already issued keep working: they are tied to the clinic, not to the environment that was thrown away.

Resetting erases everything you created there, and there is no undo. It is what you want when a test has dirtied the environment, and what you very much don't want in the middle of a test run.

Resetting belongs to whoever holds Pode administrar in the panel, or administers the clinic. Someone from outside with that role resets it alone, with nobody to ask.

The API address

Every call goes to the same address, and it is one and the same for everybody. There is no address per clinic: what tells one clinic from another is the key, never the URL.

Exampletext
https://wnricobmssysbwfhbmsm.supabase.co/functions/v1/api-v1

You'll notice a piece of the path that looks repeated. That is correct. One of them belongs to the infrastructure that hosts the API and the other to the version of ours. Copy the address exactly as it appears here; trimming the part that looks left over gets you a response saying the address doesn't exist.

First call, from zero to your first response

It takes five minutes, counting from the sign-in. Do it in test mode: if something turns out other than expected, the one who pays for it is a make-believe patient.

  1. 1Issue a test keyFollow the path described in How to get a key, ticking test mode and the catalog read scope. Copy the key.
  2. 2Store the key in an environment variableThat way the key stays out of your terminal history and out of the middle of the command you're about to send a colleague.
  3. 3Ask for the list of professionalsIt's the simplest call here: it takes no parameters at all, and the test environment already comes with a professional inside. If it answers, your key, your scope and your address are all correct at once.
  4. 4Check what came backA successful response carries the test professional. From there on the recipe is the same for everything else: the address changes, the header stays as it is.

Swap the example key for the variable you stored yours in at the previous step, or for the whole key. The one shown here is made up, and comes back refused.

Requestbash
curl -X GET \
  -H "Authorization: Bearer mv_live_a1b2c3d49f8e7d6c5b4a39281706f5e4d3c2b1a0" \
  "https://wnricobmssysbwfhbmsm.supabase.co/functions/v1/api-v1/v1/practitioners"
Status code 200
Responsejson
{
  "data": [
    {
      "id": "1f6d5c84-3b92-4a07-8e15-d6c3f9b204ae",
      "name": "Dra. Helena Costa",
      "specialty": "Dermatologia"
    }
  ]
}

If what came back isn't what you expected

Compare it against this page's catalog of refusals, which says what happened and what to do in each case. It's worth looking at the panel's Requisições (Requests) tab too, which shows the address called, the response and the time of each call that reached us. It keeps only that summary, never the content you sent.

The shape of the responses

Responses come wrapped the same way every time, and each call shows the shape of what it gives back. Program against the wrapper and ignore what you don't recognize: new information turns up over time, and an integration that ignores what it doesn't expect survives that without needing you.

The wrappers

  • A single record, inside the standard wrapper.data
  • A list, inside that same wrapper.data
  • A list in pages, together with the marker that asks for the next one.datanext_cursorMay come with no value
  • This response breaks the pattern of the others: it carries the day you asked about alongside the time slots.dateslots
  • The shape of every refusal, on any call.error

Long lists

Lists come in pages. The response carries a continuation marker: while it comes back filled in, send that same marker on the next call to fetch the next page. When it comes back with no value, you're done. Don't try to guess the total or build the paging yourself.

What each response carries

Appointment

FieldType
iduuid
statusstring
starts_atdatetime
ends_at

May come with no value

datetime
patient_id

May come with no value

uuid
professional_id

May come with no value

uuid
procedure_idsarray<uuid>
created_atdatetime

Patient

FieldType
iduuid
namestring
phone

May come with no value

string
email

May come with no value

string

Professional

FieldType
iduuid
name

May come with no value

string
specialty

May come with no value

string

Procedure

FieldType
iduuid
namestring
pricenumber
duration_minutesinteger

Free time slot

FieldType
timestring
professional_iduuid

Refusal

FieldType
codestring
messagestring
details

Doesn't always come

object

The calls

Each one brings what it takes in, what it gives back and an example ready to paste into your terminal. Swap the example's identifiers for yours.

MethodGETAddress/v1/appointments

Lists the appointments in a date range, in pages.

Scope required
appointments:readRead the schedule
Repeating
Doesn't apply
Response
Appointmentdatanext_cursorMay come with no value
ParameterTypeWhere it goes
fromOptional
Format
YYYY-MM-DD
If you don't send it
today's date, in the clinic's time zone
datequery
toOptional
Format
YYYY-MM-DD
If you don't send it
thirty days after the start date
datequery
statusOptional
Accepted values
pre_agendadoagendadoconfirmadoem_esperacanceladorealizadofaltou
stringquery
professional_idOptionaluuidquery
limitOptional
If you don't send it
50
Maximum
200
integerquery
cursorOptional
If you don't send it
the continuation marker that came with the previous page
stringquery
Request · It workedbash
curl -X GET \
  -H "Authorization: Bearer mv_live_a1b2c3d49f8e7d6c5b4a39281706f5e4d3c2b1a0" \
  "https://wnricobmssysbwfhbmsm.supabase.co/functions/v1/api-v1/v1/appointments?from=2026-10-01&to=2026-10-31&limit=2"
Status code 200
Response · It workedjson
{
  "data": [
    {
      "id": "9c1f4b2a-7e83-4d61-b0a5-2f8c6d31e4a7",
      "status": "agendado",
      "starts_at": "2026-10-03T14:30:00",
      "ends_at": "2026-10-03T15:30:00",
      "patient_id": "4d7e2c19-5a6b-4f30-9e81-c2a5b7d04f63",
      "professional_id": "1f6d5c84-3b92-4a07-8e15-d6c3f9b204ae",
      "procedure_ids": [
        "2a9f8b31-6c74-4e05-9d28-a3f1e7b5c06d"
      ],
      "created_at": "2026-09-28T11:04:22.517Z"
    },
    {
      "id": "5e0b7a94-1d62-4c38-97af-8b41d2e6c03f",
      "status": "confirmado",
      "starts_at": "2026-10-03T16:00:00",
      "ends_at": "2026-10-03T16:45:00",
      "patient_id": "4d7e2c19-5a6b-4f30-9e81-c2a5b7d04f63",
      "professional_id": "1f6d5c84-3b92-4a07-8e15-d6c3f9b204ae",
      "procedure_ids": [
        "2a9f8b31-6c74-4e05-9d28-a3f1e7b5c06d"
      ],
      "created_at": "2026-09-28T11:04:22.517Z"
    }
  ],
  "next_cursor": "MjAyNi0xMC0wM3wxNDozMDowMHw5YzFmNGIyYS03ZTgzLTRkNjEtYjBhNS0yZjhjNmQzMWU0YTc="
}
Request · Asking for the next pagebash
curl -X GET \
  -H "Authorization: Bearer mv_live_a1b2c3d49f8e7d6c5b4a39281706f5e4d3c2b1a0" \
  "https://wnricobmssysbwfhbmsm.supabase.co/functions/v1/api-v1/v1/appointments?from=2026-10-01&to=2026-10-31&limit=2&cursor=MjAyNi0xMC0wM3wxNDozMDowMHw5YzFmNGIyYS03ZTgzLTRkNjEtYjBhNS0yZjhjNmQzMWU0YTc%3D"
Status code 200
Response · Asking for the next pagejson
{
  "data": [],
  "next_cursor": null
}

MethodPOSTAddress/v1/appointments

Books an appointment for a patient, with a professional, at a given time.

Scope required
appointments:writeChange the schedule
Repeating
Requires a repeat value
Response
Appointmentdata
ParameterTypeWhere it goes
Idempotency-KeyRequiredstringheader
patient_idRequireduuidbody
professional_idRequireduuidbody
procedure_idsRequired
Minimum items
1
array<uuid>body
starts_atRequired
Format
YYYY-MM-DDTHH:MM[:SS][Z|±HH:MM]
datetimebody
statusOptional
Accepted values
pre_agendadoagendado
If you don't send it
pre_agendado
stringbody
Request · It workedbash
curl -X POST \
  -H "Authorization: Bearer mv_live_a1b2c3d49f8e7d6c5b4a39281706f5e4d3c2b1a0" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7f3c05a1-9b24-4e6d-8c17-0a5f3d92b4e8" \
  -d '{
  "patient_id": "4d7e2c19-5a6b-4f30-9e81-c2a5b7d04f63",
  "professional_id": "1f6d5c84-3b92-4a07-8e15-d6c3f9b204ae",
  "procedure_ids": [
    "2a9f8b31-6c74-4e05-9d28-a3f1e7b5c06d"
  ],
  "starts_at": "2026-10-03T14:30:00",
  "status": "agendado"
}' \
  "https://wnricobmssysbwfhbmsm.supabase.co/functions/v1/api-v1/v1/appointments"
Status code 201
Response · It workedjson
{
  "data": {
    "id": "9c1f4b2a-7e83-4d61-b0a5-2f8c6d31e4a7",
    "status": "agendado",
    "starts_at": "2026-10-03T14:30:00",
    "ends_at": "2026-10-03T15:30:00",
    "patient_id": "4d7e2c19-5a6b-4f30-9e81-c2a5b7d04f63",
    "professional_id": "1f6d5c84-3b92-4a07-8e15-d6c3f9b204ae",
    "procedure_ids": [
      "2a9f8b31-6c74-4e05-9d28-a3f1e7b5c06d"
    ],
    "created_at": "2026-09-28T11:04:22.517Z"
  }
}
Request · Without the repeat valuebash
curl -X POST \
  -H "Authorization: Bearer mv_live_a1b2c3d49f8e7d6c5b4a39281706f5e4d3c2b1a0" \
  -H "Content-Type: application/json" \
  -d '{
  "patient_id": "4d7e2c19-5a6b-4f30-9e81-c2a5b7d04f63",
  "professional_id": "1f6d5c84-3b92-4a07-8e15-d6c3f9b204ae",
  "procedure_ids": [
    "2a9f8b31-6c74-4e05-9d28-a3f1e7b5c06d"
  ],
  "starts_at": "2026-10-03T14:30:00"
}' \
  "https://wnricobmssysbwfhbmsm.supabase.co/functions/v1/api-v1/v1/appointments"
Status code 422
Response · Without the repeat valuejson
{
  "error": {
    "code": "validation_failed",
    "message": "Cabeçalho Idempotency-Key é obrigatório para criar agendamento."
  }
}

MethodGETAddress/v1/appointments/{id}

Opens one specific appointment.

Scope required
appointments:readRead the schedule
Repeating
Doesn't apply
Response
Appointmentdata
ParameterTypeWhere it goes
idRequireduuidpath
Request · It workedbash
curl -X GET \
  -H "Authorization: Bearer mv_live_a1b2c3d49f8e7d6c5b4a39281706f5e4d3c2b1a0" \
  "https://wnricobmssysbwfhbmsm.supabase.co/functions/v1/api-v1/v1/appointments/9c1f4b2a-7e83-4d61-b0a5-2f8c6d31e4a7"
Status code 200
Response · It workedjson
{
  "data": {
    "id": "9c1f4b2a-7e83-4d61-b0a5-2f8c6d31e4a7",
    "status": "agendado",
    "starts_at": "2026-10-03T14:30:00",
    "ends_at": "2026-10-03T15:30:00",
    "patient_id": "4d7e2c19-5a6b-4f30-9e81-c2a5b7d04f63",
    "professional_id": "1f6d5c84-3b92-4a07-8e15-d6c3f9b204ae",
    "procedure_ids": [
      "2a9f8b31-6c74-4e05-9d28-a3f1e7b5c06d"
    ],
    "created_at": "2026-09-28T11:04:22.517Z"
  }
}

MethodPATCHAddress/v1/appointments/{id}/status

Changes an appointment's status, within what is allowed.

Scope required
appointments:writeChange the schedule
Repeating
Safe to repeat
Response
Appointmentdata
ParameterTypeWhere it goes
idRequireduuidpath
statusRequired
Accepted values
agendadoconfirmadocancelado
stringbody
Request · It workedbash
curl -X PATCH \
  -H "Authorization: Bearer mv_live_a1b2c3d49f8e7d6c5b4a39281706f5e4d3c2b1a0" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "confirmado"
}' \
  "https://wnricobmssysbwfhbmsm.supabase.co/functions/v1/api-v1/v1/appointments/9c1f4b2a-7e83-4d61-b0a5-2f8c6d31e4a7/status"
Status code 200
Response · It workedjson
{
  "data": {
    "id": "9c1f4b2a-7e83-4d61-b0a5-2f8c6d31e4a7",
    "status": "confirmado",
    "starts_at": "2026-10-03T14:30:00",
    "ends_at": "2026-10-03T15:30:00",
    "patient_id": "4d7e2c19-5a6b-4f30-9e81-c2a5b7d04f63",
    "professional_id": "1f6d5c84-3b92-4a07-8e15-d6c3f9b204ae",
    "procedure_ids": [
      "2a9f8b31-6c74-4e05-9d28-a3f1e7b5c06d"
    ],
    "created_at": "2026-09-28T11:04:22.517Z"
  }
}
Request · A status change that was refusedbash
curl -X PATCH \
  -H "Authorization: Bearer mv_live_a1b2c3d49f8e7d6c5b4a39281706f5e4d3c2b1a0" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "realizado"
}' \
  "https://wnricobmssysbwfhbmsm.supabase.co/functions/v1/api-v1/v1/appointments/9c1f4b2a-7e83-4d61-b0a5-2f8c6d31e4a7/status"
Status code 422
Response · A status change that was refusedjson
{
  "error": {
    "code": "status_transition_not_allowed",
    "message": "Transição de status não permitida por esta API.",
    "details": {
      "from": "agendado",
      "to": "realizado"
    }
  }
}

MethodPOSTAddress/v1/appointments/{id}/cancel

Cancels an appointment.

Scope required
appointments:writeChange the schedule
Repeating
Safe to repeat
Response
Appointmentdata
ParameterTypeWhere it goes
idRequireduuidpath
Request · It workedbash
curl -X POST \
  -H "Authorization: Bearer mv_live_a1b2c3d49f8e7d6c5b4a39281706f5e4d3c2b1a0" \
  "https://wnricobmssysbwfhbmsm.supabase.co/functions/v1/api-v1/v1/appointments/9c1f4b2a-7e83-4d61-b0a5-2f8c6d31e4a7/cancel"
Status code 200
Response · It workedjson
{
  "data": {
    "id": "9c1f4b2a-7e83-4d61-b0a5-2f8c6d31e4a7",
    "status": "cancelado",
    "starts_at": "2026-10-03T14:30:00",
    "ends_at": "2026-10-03T15:30:00",
    "patient_id": "4d7e2c19-5a6b-4f30-9e81-c2a5b7d04f63",
    "professional_id": "1f6d5c84-3b92-4a07-8e15-d6c3f9b204ae",
    "procedure_ids": [
      "2a9f8b31-6c74-4e05-9d28-a3f1e7b5c06d"
    ],
    "created_at": "2026-09-28T11:04:22.517Z"
  }
}

MethodGETAddress/v1/patients/{id}

Opens a patient's basic record.

Scope required
patients:readRead a patient
Repeating
Doesn't apply
Response
Patientdata
ParameterTypeWhere it goes
idRequireduuidpath
Request · It workedbash
curl -X GET \
  -H "Authorization: Bearer mv_live_a1b2c3d49f8e7d6c5b4a39281706f5e4d3c2b1a0" \
  "https://wnricobmssysbwfhbmsm.supabase.co/functions/v1/api-v1/v1/patients/4d7e2c19-5a6b-4f30-9e81-c2a5b7d04f63"
Status code 200
Response · It workedjson
{
  "data": {
    "id": "4d7e2c19-5a6b-4f30-9e81-c2a5b7d04f63",
    "name": "Ana Ribeiro",
    "phone": "+5511987654321",
    "email": "ana.ribeiro@example.com"
  }
}

MethodGETAddress/v1/practitioners

Lists the clinic's professionals.

Scope required
catalog:readRead the catalog
Repeating
Doesn't apply
Response
Professionaldata

This call takes no parameters.

Request · It workedbash
curl -X GET \
  -H "Authorization: Bearer mv_live_a1b2c3d49f8e7d6c5b4a39281706f5e4d3c2b1a0" \
  "https://wnricobmssysbwfhbmsm.supabase.co/functions/v1/api-v1/v1/practitioners"
Status code 200
Response · It workedjson
{
  "data": [
    {
      "id": "1f6d5c84-3b92-4a07-8e15-d6c3f9b204ae",
      "name": "Dra. Helena Costa",
      "specialty": "Dermatologia"
    }
  ]
}

MethodGETAddress/v1/procedures

Lists the procedures in the catalog.

Scope required
catalog:readRead the catalog
Repeating
Doesn't apply
Response
Proceduredata

This call takes no parameters.

Request · It workedbash
curl -X GET \
  -H "Authorization: Bearer mv_live_a1b2c3d49f8e7d6c5b4a39281706f5e4d3c2b1a0" \
  "https://wnricobmssysbwfhbmsm.supabase.co/functions/v1/api-v1/v1/procedures"
Status code 200
Response · It workedjson
{
  "data": [
    {
      "id": "2a9f8b31-6c74-4e05-9d28-a3f1e7b5c06d",
      "name": "Limpeza de pele profunda",
      "price": 280,
      "duration_minutes": 60
    }
  ]
}

MethodGETAddress/v1/availability

Shows a day's free time slots, and you can narrow it down to one professional or one procedure.

Scope required
catalog:readRead the catalog
Repeating
Doesn't apply
Response
Free time slotdateslots
ParameterTypeWhere it goes
dateRequired
Format
YYYY-MM-DD
datequery
professional_idOptionaluuidquery
procedure_idOptionaluuidquery
Request · It workedbash
curl -X GET \
  -H "Authorization: Bearer mv_live_a1b2c3d49f8e7d6c5b4a39281706f5e4d3c2b1a0" \
  "https://wnricobmssysbwfhbmsm.supabase.co/functions/v1/api-v1/v1/availability?date=2026-10-03&procedure_id=2a9f8b31-6c74-4e05-9d28-a3f1e7b5c06d"
Status code 200
Response · It workedjson
{
  "date": "2026-10-03",
  "slots": [
    {
      "time": "09:00",
      "professional_id": "1f6d5c84-3b92-4a07-8e15-d6c3f9b204ae"
    },
    {
      "time": "10:00",
      "professional_id": "1f6d5c84-3b92-4a07-8e15-d6c3f9b204ae"
    }
  ]
}

Repeating without duplicating

The network drops in the middle of the call, the timeout runs out, and you're left not knowing whether the appointment was created. Repeating on your own can book the same patient twice.

That's why every creation travels with a repeat value you make up yourself, one per operation. If it turns up again with the same content, Meevia hands back the answer it already gave, without processing anything a second time.

Idempotency-Key

How to choose that value

Generate a unique value per operation (a random identifier does the job) and store it alongside your attempt, so you can reuse exactly the same one when the time comes to repeat. Generating a new one on every attempt is the same as having no protection at all.

The stored answer comes back as it was, including when it was a refusal. Repeating with the value of an attempt that went wrong gives you the same error, not a fresh attempt.

Reusing the same value with different content is refused, and that refusal usually gives away two distinct operations sharing one value. The same response turns up when a twin call is still in flight, and there waiting a few moments and repeating sorts it out.

The value is good for one day. After that it is forgotten, and a repeat goes back to being processed for real.

An attempt left without a response is given up for lost after a few minutes, and the next call carrying that value takes its place. That way a request that died halfway doesn't block you until the following day.

Minutes to wait before repeating an attempt that got no response
5

What happens when you repeat each call

Doesn't apply

It's a read, and a read changes nothing by being repeated.

Requires a repeat value

Without it the call is refused. It's the protection against booking the same patient twice when the network fails halfway.

Safe to repeat

Repeating lands in the same place: asking for the status the appointment is already in gives you the appointment as it stands. There's no need to send a repeat value here.

Usage limits

Each key has a ceiling of calls per minute of its own. It's per key, not per clinic. Two integrations in the same clinic don't compete for one limit, and one that fired off too many calls doesn't take the other down.

Calls per minute, on each key
120

The count resets as each clock minute turns over, not on a window that slides. In practice, two bursts fired around the turn land in different minutes, so a short spike can get through even when it adds up to more than the ceiling. This isn't slack you can count on: what keeps an integration standing is its average pace.

When you go over, the response tells you how many seconds to wait in a header of its own, not in the body. Respect that number instead of trying again straight away. If this becomes routine, the ceiling isn't the problem: space the calls out, keep a copy of what changes little (the catalog changes little) and stop asking in a loop.

Retry-After

Status changes that are allowed

The API accepts a closed set of changes, and the table shows which: each starting status has the destinations it allows, and anything absent from the table is refused. Marking an appointment as done and recording a no-show are left out by product decision. Both generate a sale, a charge and the use of a session from a package. They stay in the hands of the people running the clinic.

Current statusCan change to
pre_agendadoagendadoconfirmadocancelado
agendadoconfirmadocancelado
confirmadocancelado
em_esperacancelado

The statuses

pre_agendado
Booked, but not yet confirmed by anybody. It's the status a new appointment is born in when you don't ask for another.
agendado
It's on the schedule and the clinic is counting on it.
confirmado
The patient has confirmed they're coming. It's what the front desk looks at to know what to expect on the day.
em_espera
It's in the queue, waiting for a slot to open. The API puts nobody here, but it does read who is, and from here the only possible change is cancelling.
cancelado
Cancelled, and that's final: from here the API changes nothing more.
realizado
The appointment happened. The clinic is who records that, because the change generates the sale. The API reads it and doesn't write it.
faltou
The patient didn't turn up. This one belongs to the clinic too: it feeds the no-show charge and the use of a session from a package. The API reads it and doesn't write it.

When a call doesn't work out

Every refusal comes back in the same shape, with a short code that doesn't change and a message in Portuguese. In your program, always decide by the code, never by the message: the message is there for your log and can be rewritten at any moment.

Some refusals also carry a breakdown of what didn't pass. Not every refusal carries that breakdown, so your code can't depend on it.

unauthorizedStatus code 401

The key wasn't accepted. It may not have arrived, it may have arrived wrong, it may have been revoked or it may have expired, and the response is the same in all four cases.

Start here, because this is the one that confuses people most: a call refused for an unrecognized key does not appear in the Requisições tab, since without a valid key there's no way to know which clinic it belonged to. Not finding your call there doesn't mean it never arrived. That said, check that the header really is being sent and that the value is the complete key, not just the secret part, with no stray space and no line break in the middle (copying from a text editor usually drags one along). Check as well that it isn't this page's example key, which is made up. If the key is an old one, look in the panel's Chaves tab to see whether it's still active. If it persists, issue another: that's quicker than investigating.

plan_requiredStatus code 403

The key is valid, but the clinic's plan doesn't include access to the API.

No change to your code will fix this. Let the clinic know, and they take it to Meevia support: it's a matter of the plan.

scope_requiredStatus code 403

The key is valid, but it doesn't carry the scope this call requires.

Look at the call's entry to see which scope it asks for, and compare that with the key's scopes in the panel's Chaves tab. Scopes can't be edited: issue another key with what was missing, swap it in your environment and revoke the old one.

not_foundStatus code 404

Two different situations land here: either the address matches nothing, or what you asked for doesn't exist in this clinic.

Check the address first, letter by letter, against the call's example; one plural too many lands you here. If the address is right, then it's the record that's missing: it may have been deleted, or it may belong to another clinic. Remember too that the test environment has data of its own. An identifier copied from the real clinic doesn't exist inside it.

method_not_allowedStatus code 405

The address exists, but it doesn't answer that kind of request.

Check the call's entry for the method it expects. Nearly always this is a read attempted as a write, or the other way round.

validation_failedStatus code 422

Something in what you sent didn't pass the check: a required value is missing, or one of them arrived in a shape that doesn't work.

The refusal's message says what didn't pass. Compare it with the call's parameter table, paying particular attention to the date and time format, which is where most mistakes happen.

rate_limitedStatus code 429

You went past that key's ceiling of calls per minute.

Wait the number of seconds that comes in the response's wait header. It isn't in the body. That's why so many people never find the number and end up trying again straight away, which only digs the hole deeper. If it happens often, adjust your pace: space the calls out, keep a copy of what changes little and stop asking in a loop.

idempotency_conflictStatus code 409

The repeat value you sent has already been used for something else, or a twin call is still in flight.

If it was something else, start generating a new value for every new operation; reusing one across different operations is what causes this. If it's the twin, wait a few moments and repeat with the same value: as soon as the first one finishes, the stored answer is handed back.

status_transition_not_allowedStatus code 422

The change you asked for isn't allowed from the status the appointment is in.

Read the table of allowed changes, which shows where you can go from each status. Marking as done and recording a no-show aren't in it, and remain the clinic's own operation. The refusal comes back with the current status and the one you asked for, which usually reveals that the appointment had already moved somewhere else.

sandbox_not_readyStatus code 409

The key is a test key and the clinic's test environment isn't up yet.

Try again in a few moments. If it persists, the environment needs resetting, which rebuilds it from scratch, and you do that yourself if you hold Pode administrar in the panel. A production key never lands here.

internal_errorStatus code 500

Something broke on our side.

It isn't your code. Try again after a pause. If it persists, talk to Meevia support and give them the time and what you called; the panel's Requisições tab helps to pin it down.

Back to top

Need a hand

Tell us what you called, what you expected and what came back. The panel's Requisições tab shows what reached us, and that's where we start looking.

This page and the API

If you find a difference between what's written here and what the API answered, the API is right, and we want to hear about it. The date below says when this page was last checked against it.

Chat on WhatsApp