# Prescrição embarcada Amplimed — V2: Prescrição embarcada por bundleJS

Autentique o parceiro, crie a sessão de prescrição e incorpore o bundle na sua aplicação.

## Sobre este arquivo

Este é o manual de integração V2 inteiro em um único arquivo, gerado a partir do contrato da integração. Ele existe para ser lido por um assistente de IA: cole o arquivo no assistente que você usa e peça o código de integração na sua linguagem. Os exemplos de chamada estão em cURL — método, caminho, headers e corpo estão completos, então a tradução para qualquer stack é direta.

| | |
|---|---|
| Versão da integração | V2 (Nova versão) |
| Base URL | `https://devprescription.amplimed.cloud/identity/v1` |
| Ambiente | desenvolvimento |
| Versão do contrato | `v1` |
| Bundle JS | `https://devprescription.amplimed.cloud/bundle/ampliprescreve.umd.js` |
| Página de origem | https://embeddedtest.ampli.li/integracao/v2 |
| Gerado em | 30/09/2026 12:15 |

## Passo a passo

### 1. Autentique o parceiro

`POST https://devprescription.amplimed.cloud/identity/v1/partners/authenticate`

Troque clientId e secretKey por um token de curta duração.

A integração começa trocando as credenciais do parceiro por um **token de curta duração**. A API confere o `clientId` e compara a `secretKey` com o hash guardado no cadastro do parceiro. Esse token existe por um único motivo: autorizar a criação da sessão de prescrição na etapa seguinte. Ele não dá acesso a dado de paciente nem é o token que o front embarcado usa.

> **Esta chamada é server-to-server**
>
> A `secretKey` nunca pode sair do backend do parceiro. Não a coloque em JavaScript, em aplicativo mobile, em variável exposta ao browser nem em repositório — quem tem a secretKey consegue abrir sessões em nome do seu serviço.

#### Onde guardar as credenciais

Em variável de ambiente ou em um gerenciador de segredos (AWS Secrets Manager, Vault, o que já existir na sua stack). Em Laravel, isso significa `.env` lido por `config()` — nunca o valor escrito direto no código. O `.env` não vai para o versionamento.

#### O que fazer quando vier 401

Um `401` significa que a credencial não confere — e repetir a chamada não vai mudar isso. Não coloque retry automático em loop aqui. Confira, nesta ordem: se você está apontando para o ambiente certo, se o `clientId` é o do ambiente em questão, e se a `secretKey` não foi rotacionada. Retry com backoff faz sentido para 5xx e timeout, não para 401.

#### Rotação da secretKey

A `secretKey` pode ser trocada a qualquer momento a pedido do parceiro, e deve ser trocada imediatamente se houver suspeita de vazamento. Como ela é lida do ambiente, a rotação é: gerar a nova credencial com o time da Amplimed, atualizar o segredo no seu ambiente e reiniciar a aplicação. Tokens já emitidos continuam válidos até expirar, então a troca não derruba sessões em andamento.

#### Cache do token

O token é de curta duração. Guardá-lo em memória ou em cache pelo tempo de vida informado em `expiresIn` evita uma chamada de autenticação a cada sessão criada. Sempre respeite o `expiresAt` devolvido em vez de assumir um prazo fixo.

#### Corpo da requisição

| Campo | Tipo | Obrigatório | Detalhes |
|---|---|---|---|
| `clientId` | `string` | sim |  |
| `secretKey` | `string` | sim |  |

```json
{
    "clientId": "SEU_CLIENT_ID",
    "secretKey": "SUA_SECRET_KEY"
}
```

#### Resposta 200

`PartnerTokenResource`

| Campo | Tipo | Obrigatório | Detalhes |
|---|---|---|---|
| `token` | `string` | sim |  |
| `tokenType` | `string` | sim | valores: `Bearer` |
| `expiresIn` | `string` | sim |  |
| `expiresAt` | `string` | sim |  |

```json
{
    "token": "eyJhbGciOiJIUzI1NiJ9.TOKEN_DE_PARCEIRO.ASSINATURA",
    "tokenType": "Bearer",
    "expiresIn": "300",
    "expiresAt": "2026-01-01T00:05:00Z"
}
```

#### Erros

| Status | Significado |
|---|---|
| `401` | clientId ou secretKey inválidos. |
| `422` | Validation error |

#### Exemplo

```bash
curl -X POST 'https://devprescription.amplimed.cloud/identity/v1/partners/authenticate' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{"clientId":"SEU_CLIENT_ID","secretKey":"SUA_SECRET_KEY"}'
```

### 2. Crie a sessão de prescrição

`POST https://devprescription.amplimed.cloud/identity/v1/sessions`

Envie profissional e paciente e receba o embeddedToken.

> Requer o token de parceiro no header `Authorization: Bearer <token>`.

Com o token de parceiro em mãos, você cria a sessão de prescrição. A API resolve o escopo de módulos, valida os `customFields` contra o schema acordado no seu cadastro, grava um snapshot do contexto (parceiro, profissional, paciente) e devolve o `embeddedToken`.

> **Token de parceiro e embeddedToken são coisas diferentes**
>
> Esta é de longe a maior fonte de confusão de quem integra pela primeira vez:
>
> - O **token de parceiro** (etapa 1) identifica *a sua empresa*. Ele vive no seu backend, autoriza a criação de sessões e não deve chegar ao browser.
> - O **embeddedToken** (esta etapa) identifica *uma sessão específica* — este profissional, este paciente, estes escopos, este prazo. É ele, e apenas ele, que vai para o bundle embarcado no front.
>
> Mandar o token de parceiro para o front é um erro de segurança; mandar o `embeddedToken` para `POST /sessions` simplesmente não funciona.

#### Como os scopes são resolvidos

Os módulos liberados na sessão são o **cruzamento** de duas listas: o piso regulatório do conselho do profissional (o que aquele conselho permite prescrever) e o teto comercial do parceiro (o que a sua empresa contratou). A sessão recebe a interseção.

Na prática: se o seu contrato inclui um módulo que o conselho daquele profissional não autoriza, ele não aparece nos `scopes`. E se o profissional poderia prescrever algo que a sua empresa não contratou, também não aparece. Por isso o `register.type` é obrigatório — é ele que determina o piso. Leia sempre os `scopes` devolvidos em vez de assumir o que foi contratado.

#### customFields

Os `customFields` precisam bater com o `customFieldsSchema` definido no cadastro do seu parceiro. Campo fora do schema combinado faz a chamada voltar `422`. Se você precisa de um campo novo, ele é acordado no cadastro primeiro — não basta começar a enviá-lo.

#### professionalId e patientId

São identificadores **da Amplimed**, não do seu sistema. Envie o id quando o profissional ou o paciente já existir no nosso cadastro. Se ainda não existir, mande `null` — o contrato aceita `null` nos dois, e nesse caso quem identifica a pessoa é o resto do payload: nome, documento e, no profissional, o registro do conselho.

#### Ciclo de vida da sessão

A sessão nasce em `createdAt` e vale até `expiresAt`, conforme o TTL configurado para o parceiro. Enquanto o profissional estiver trabalhando, a renovação desliza esse prazo e devolve um `embeddedToken` novo. Quando o atendimento termina, encerre a sessão em vez de deixá-la expirar sozinha — assim ela deixa de ser utilizável imediatamente. Uma sessão por atendimento: não reaproveite a mesma sessão para outro paciente.

> **LGPD: dado de paciente trafega aqui**
>
> Este payload carrega nome, documento, data de nascimento, contato e endereço do paciente. Não registre o corpo cru desta requisição em log, APM, ferramenta de erro ou histórico de requisições. Se precisar de rastreio, registre apenas identificadores (`sessionId`, `patientId`) e o status da resposta.

#### Corpo da requisição

| Campo | Tipo | Obrigatório | Detalhes |
|---|---|---|---|
| `professional` | `object` | sim |  |
| `professional.professionalId` | `string` | não | aceita `null` |
| `professional.name` | `string` | sim |  |
| `professional.register` | `object` | sim |  |
| `professional.register.type` | `string` | sim |  |
| `professional.register.value` | `string` | sim |  |
| `professional.register.state` | `string` | sim |  |
| `professional.specialty` | `string` | não | aceita `null` |
| `professional.document` | `object` | sim |  |
| `professional.document.type` | `string` | sim |  |
| `professional.document.value` | `string` | sim |  |
| `patient` | `object` | sim |  |
| `patient.patientId` | `string` | não | aceita `null` |
| `patient.name` | `string` | sim |  |
| `patient.document` | `object` | sim |  |
| `patient.document.type` | `string` | sim | valores: `cpf`, `passport`, `rg` |
| `patient.document.value` | `string` | sim |  |
| `patient.birthDate` | `string (date-time)` | não | aceita `null` |
| `patient.gender` | `string` | não | aceita `null` |
| `patient.phone` | `string` | não | aceita `null` |
| `patient.email` | `string (email)` | não | aceita `null` |
| `patient.address` | `object` | não | aceita `null` |
| `patient.address.street` | `string` | não |  |
| `patient.address.number` | `string` | não |  |
| `patient.address.complement` | `string` | não | aceita `null` |
| `patient.address.neighborhood` | `string` | não |  |
| `patient.address.city` | `string` | não |  |
| `patient.address.state` | `string` | não |  |
| `patient.address.zipCode` | `string` | não |  |
| `patient.address.country` | `string` | não |  |
| `customFields` | `string[]` | não | aceita `null` |
| `embeddedType` | `string` | não | aceita `null` |

```json
{
    "professional": {
        "professionalId": null,
        "name": "Dra. Ana Souza",
        "register": {
            "type": "CRM",
            "value": "123456",
            "state": "SC"
        },
        "specialty": "Clínica Médica",
        "document": {
            "type": "cpf",
            "value": "00000000000"
        }
    },
    "patient": {
        "patientId": null,
        "name": "João da Silva",
        "document": {
            "type": "cpf",
            "value": "11111111111"
        },
        "birthDate": "1985-04-12T00:00:00Z",
        "gender": "male",
        "phone": "+5548999999999",
        "email": "joao@exemplo.com",
        "address": {
            "street": "Rua das Flores",
            "number": "100",
            "complement": null,
            "neighborhood": "Centro",
            "city": "Florianópolis",
            "state": "SC",
            "zipCode": "88010000",
            "country": "BR"
        }
    },
    "customFields": [],
    "embeddedType": null
}
```

#### Resposta 201

Sessão de prescrição criada.

| Campo | Tipo | Obrigatório | Detalhes |
|---|---|---|---|
| `sessionId` | `string` | sim |  |
| `embeddedToken` | `string` | sim | aceita `null` |
| `embeddedType` | `string` | sim | aceita `null` |
| `status` | `string` | sim | aceita `null` |
| `apiVersion` | `string` | sim | aceita `null` |
| `scopes` | `string` | sim | aceita `null` |
| `featureFlags` | `string` | sim | aceita `null` |
| `createdAt` | `string` | sim |  |
| `expiresAt` | `string` | sim |  |

```json
{
    "sessionId": "65f0c3a1d4e5b6a7c8d9e0f1",
    "embeddedToken": "eyJhbGciOiJIUzI1NiJ9.TOKEN_DA_SESSAO.ASSINATURA",
    "embeddedType": null,
    "status": "active",
    "apiVersion": "v1",
    "scopes": "<string>",
    "featureFlags": "<string>",
    "createdAt": "2026-01-01T00:00:00Z",
    "expiresAt": "2026-01-01T00:05:00Z"
}
```

#### Erros

| Status | Significado |
|---|---|
| `422` | customFields fora do customFieldsSchema do parceiro, ou payload inválido. |

#### Exemplo

```bash
curl -X POST 'https://devprescription.amplimed.cloud/identity/v1/sessions' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer SEU_TOKEN_DE_PARCEIRO' \
  -H 'Content-Type: application/json' \
  -d '{"professional":{"professionalId":null,"name":"Dra. Ana Souza","register":{"type":"CRM","value":"123456","state":"SC"},"specialty":"Clínica Médica","document":{"type":"cpf","value":"00000000000"}},"patient":{"patientId":null,"name":"João da Silva","document":{"type":"cpf","value":"11111111111"},"birthDate":"1985-04-12T00:00:00Z","gender":"male","phone":"+5548999999999","email":"joao@exemplo.com","address":{"street":"Rua das Flores","number":"100","complement":null,"neighborhood":"Centro","city":"Florianópolis","state":"SC","zipCode":"88010000","country":"BR"}},"customFields":[],"embeddedType":null}'
```

### 3. Incorpore o bundle

*Etapa de front-end: não é uma chamada HTTP.*

Carregue o bundle e inicialize com o embeddedToken.

A prescrição V2 roda como um **bundle JavaScript** montado dentro de um elemento da sua página — não é iframe. Você carrega o script, cria um elemento host e chama `Ampliprescreve.init()` passando o `embeddedToken` da sessão criada na etapa anterior.

> **Só o embeddedToken vai para o front**
>
> O `sessionToken` que o bundle recebe é o `embeddedToken` devolvido por `POST /sessions` — nunca o token de parceiro da etapa 1. O token de parceiro autoriza criar sessões em nome da sua empresa: se ele chegar ao browser, qualquer pessoa com acesso à página pode abrir sessões para qualquer paciente.

#### Como importar o bundle

O script é servido pela Amplimed e a URL varia por ambiente — trate-a como configuração, não como constante no código. Neste ambiente ela é `https://devprescription.amplimed.cloud/bundle/ampliprescreve.umd.js`.

Em página tradicional, uma `<script>` no HTML resolve. Em SPA, você pode deixá-la no `index.html` ou carregá-la sob demanda, só quando a prescrição for aberta — é o que os exemplos de React e Vue fazem, guardando a promessa do carregamento para o script não entrar na página mais de uma vez.

#### shadow e tema

Com `shadow: true` o bundle monta dentro de um shadow DOM, isolando o CSS da prescrição do CSS da sua aplicação — é o recomendado, porque evita que o seu reset ou framework de estilo vaze para dentro da prescrição. O `theme` aceita `colorPrimary` e `radius` para a prescrição acompanhar a identidade visual do parceiro.

> **Reabrir a prescrição exige um host novo**
>
> `attachShadow` só pode ser chamado uma vez por elemento. Se você inicializar o bundle no mesmo elemento de novo — ao reabrir a prescrição, por exemplo — a chamada falha porque já existe um shadow tree ali. Substitua o host por um clone vazio antes de reinicializar, como no exemplo ao lado.

#### Quando a sessão expira

O `embeddedToken` vale até o `expiresAt` da sessão. Se o atendimento for longo, renove a sessão pelo seu backend e reinicialize o bundle com o token novo — veja as operações opcionais.

```html
<!-- Página tradicional: o bundle entra por script tag. -->
<div id="prescription-bundle"></div>

<script src="https://devprescription.amplimed.cloud/bundle/ampliprescreve.umd.js"></script>
<script>
  // O embeddedToken vem do seu backend, de POST /sessions.
  Ampliprescreve.init(document.getElementById('prescription-bundle'), {
    sessionToken: embeddedToken,
    shadow: true,
    theme: { colorPrimary: '#6366F1', radius: '8px' },
  });
</script>
```

```javascript
const BUNDLE_URL = 'https://devprescription.amplimed.cloud/bundle/ampliprescreve.umd.js';

// Carrega o bundle uma única vez, venha de onde vier a chamada.
let bundle = null;

function carregarBundle() {
  if (window.Ampliprescreve) return Promise.resolve();

  bundle ??= new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = BUNDLE_URL;
    script.onload = resolve;
    script.onerror = () => reject(new Error('Falha ao carregar o bundle da prescrição.'));
    document.head.appendChild(script);
  });

  return bundle;
}

// O embeddedToken vem do seu backend, de POST /sessions.
const { embeddedToken } = await criarSessaoNoSeuBackend();
await carregarBundle();

let host = document.getElementById('prescription-bundle');

// attachShadow só roda uma vez por elemento: troque o host por um clone
// vazio antes de reinicializar, senão reabrir a prescrição falha.
host.replaceWith(host.cloneNode(false));
host = document.getElementById('prescription-bundle');

Ampliprescreve.init(host, {
  sessionToken: embeddedToken,
  shadow: true,
  theme: { colorPrimary: '#6366F1', radius: '8px' },
});
```

```jsx
import { useEffect, useRef } from 'react';

const BUNDLE_URL = 'https://devprescription.amplimed.cloud/bundle/ampliprescreve.umd.js';

// Fora do componente: o bundle é carregado uma vez por página, não por
// montagem. Alternativa: deixar o <script> no index.html da aplicação.
let bundle = null;

function carregarBundle() {
  if (window.Ampliprescreve) return Promise.resolve();

  bundle ??= new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = BUNDLE_URL;
    script.onload = resolve;
    script.onerror = () => reject(new Error('Falha ao carregar o bundle da prescrição.'));
    document.head.appendChild(script);
  });

  return bundle;
}

export function Prescricao({ embeddedToken }) {
  const container = useRef(null);

  useEffect(() => {
    if (!embeddedToken) return;

    let cancelado = false;

    carregarBundle().then(() => {
      if (cancelado || !container.current) return;

      // Um host novo a cada init: attachShadow não pode rodar duas vezes no
      // mesmo elemento. Isso também cobre o efeito rodar duas vezes no
      // StrictMode em desenvolvimento.
      const host = document.createElement('div');
      container.current.replaceChildren(host);

      window.Ampliprescreve.init(host, {
        sessionToken: embeddedToken,
        shadow: true,
        theme: { colorPrimary: '#6366F1', radius: '8px' },
      });
    });

    return () => {
      cancelado = true;
      container.current?.replaceChildren();
    };
  }, [embeddedToken]);

  return <div ref={container} />;
}
```

```vue
<script setup>
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';

const BUNDLE_URL = 'https://devprescription.amplimed.cloud/bundle/ampliprescreve.umd.js';

// Fora do componente: o bundle é carregado uma vez por página, não por
// montagem. Alternativa: deixar o <script> no index.html da aplicação.
let bundle = null;

function carregarBundle() {
  if (window.Ampliprescreve) return Promise.resolve();

  bundle ??= new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = BUNDLE_URL;
    script.onload = resolve;
    script.onerror = () => reject(new Error('Falha ao carregar o bundle da prescrição.'));
    document.head.appendChild(script);
  });

  return bundle;
}

const props = defineProps({ embeddedToken: String });
const container = ref(null);

async function montar() {
  if (!props.embeddedToken) return;

  await carregarBundle();
  if (!container.value) return;

  // Um host novo a cada init: attachShadow não pode rodar duas vezes no
  // mesmo elemento.
  const host = document.createElement('div');
  container.value.replaceChildren(host);

  window.Ampliprescreve.init(host, {
    sessionToken: props.embeddedToken,
    shadow: true,
    theme: { colorPrimary: '#6366F1', radius: '8px' },
  });
}

onMounted(montar);
watch(() => props.embeddedToken, montar);
onBeforeUnmount(() => container.value?.replaceChildren());
</script>

<template>
  <div ref="container"></div>
</template>
```

## Operações opcionais

Com as etapas acima a integração já funciona de ponta a ponta. As operações abaixo servem para manter a sessão viva além do prazo inicial e para encerrá-la explicitamente.

### Valide a sessão

`GET https://devprescription.amplimed.cloud/identity/v1/sessions/validate`

Confirme que o embeddedToken ainda vale e recupere o contexto da sessão.

#### Corpo da requisição

*Esta operação não recebe corpo.*

#### Resposta 200

`SessionContextResource`

| Campo | Tipo | Obrigatório | Detalhes |
|---|---|---|---|
| `sessionId` | `string` | sim |  |
| `embeddedToken` | `string` | sim |  |
| `partner` | `object` | sim |  |
| `professional` | `object` | sim |  |
| `patient` | `object` | sim |  |
| `scopes` | `any[]` | sim |  |
| `featureFlags` | `any[]` | sim |  |
| `customFields` | `any[]` | sim |  |
| `status` | `string` | sim |  |
| `apiVersion` | `string` | sim |  |
| `expiresAt` | `string` | sim |  |

```json
{
    "sessionId": "65f0c3a1d4e5b6a7c8d9e0f1",
    "embeddedToken": "eyJhbGciOiJIUzI1NiJ9.TOKEN_DA_SESSAO.ASSINATURA",
    "partner": [],
    "professional": [],
    "patient": [],
    "scopes": [],
    "featureFlags": [],
    "customFields": [],
    "status": "active",
    "apiVersion": "v1",
    "expiresAt": "2026-01-01T00:05:00Z"
}
```

#### Erros

| Status | Significado |
|---|---|
| `404` | Nenhuma sessão encontrada para o embeddedToken informado. |
| `410` | Sessão encontrada mas não está mais ativa (expirada ou encerrada). |
| `422` | Validation error |

#### Exemplo

```bash
curl -X GET 'https://devprescription.amplimed.cloud/identity/v1/sessions/validate' \
  -H 'Accept: application/json'
```

### Renove a sessão

`POST https://devprescription.amplimed.cloud/identity/v1/sessions/renew`

Renovação deslizante do expiresAt, devolve um embeddedToken novo.

#### Corpo da requisição

| Campo | Tipo | Obrigatório | Detalhes |
|---|---|---|---|
| `embeddedToken` | `string` | sim |  |

```json
{
    "embeddedToken": "eyJhbGciOiJIUzI1NiJ9.TOKEN_DA_SESSAO.ASSINATURA"
}
```

#### Resposta 200

`SessionContextResource`

| Campo | Tipo | Obrigatório | Detalhes |
|---|---|---|---|
| `sessionId` | `string` | sim |  |
| `embeddedToken` | `string` | sim |  |
| `partner` | `object` | sim |  |
| `professional` | `object` | sim |  |
| `patient` | `object` | sim |  |
| `scopes` | `any[]` | sim |  |
| `featureFlags` | `any[]` | sim |  |
| `customFields` | `any[]` | sim |  |
| `status` | `string` | sim |  |
| `apiVersion` | `string` | sim |  |
| `expiresAt` | `string` | sim |  |

```json
{
    "sessionId": "65f0c3a1d4e5b6a7c8d9e0f1",
    "embeddedToken": "eyJhbGciOiJIUzI1NiJ9.TOKEN_DA_SESSAO.ASSINATURA",
    "partner": [],
    "professional": [],
    "patient": [],
    "scopes": [],
    "featureFlags": [],
    "customFields": [],
    "status": "active",
    "apiVersion": "v1",
    "expiresAt": "2026-01-01T00:05:00Z"
}
```

#### Erros

| Status | Significado |
|---|---|
| `404` | Nenhuma sessão encontrada para o embeddedToken informado. |
| `410` | Sessão encontrada mas não está mais ativa (expirada ou encerrada). |
| `422` | Validation error |

#### Exemplo

```bash
curl -X POST 'https://devprescription.amplimed.cloud/identity/v1/sessions/renew' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{"embeddedToken":"eyJhbGciOiJIUzI1NiJ9.TOKEN_DA_SESSAO.ASSINATURA"}'
```

### Encerre a sessão

`POST https://devprescription.amplimed.cloud/identity/v1/sessions/end`

Finaliza a sessão de prescrição antes do prazo de expiração.

#### Corpo da requisição

| Campo | Tipo | Obrigatório | Detalhes |
|---|---|---|---|
| `embeddedToken` | `string` | sim |  |

```json
{
    "embeddedToken": "eyJhbGciOiJIUzI1NiJ9.TOKEN_DA_SESSAO.ASSINATURA"
}
```

#### Resposta 200

`SessionContextResource`

| Campo | Tipo | Obrigatório | Detalhes |
|---|---|---|---|
| `sessionId` | `string` | sim |  |
| `embeddedToken` | `string` | sim |  |
| `partner` | `object` | sim |  |
| `professional` | `object` | sim |  |
| `patient` | `object` | sim |  |
| `scopes` | `any[]` | sim |  |
| `featureFlags` | `any[]` | sim |  |
| `customFields` | `any[]` | sim |  |
| `status` | `string` | sim |  |
| `apiVersion` | `string` | sim |  |
| `expiresAt` | `string` | sim |  |

```json
{
    "sessionId": "65f0c3a1d4e5b6a7c8d9e0f1",
    "embeddedToken": "eyJhbGciOiJIUzI1NiJ9.TOKEN_DA_SESSAO.ASSINATURA",
    "partner": [],
    "professional": [],
    "patient": [],
    "scopes": [],
    "featureFlags": [],
    "customFields": [],
    "status": "active",
    "apiVersion": "v1",
    "expiresAt": "2026-01-01T00:05:00Z"
}
```

#### Erros

| Status | Significado |
|---|---|
| `404` | Nenhuma sessão encontrada para o embeddedToken informado. |
| `410` | Sessão encontrada mas não está mais ativa (expirada ou encerrada). |
| `422` | Validation error |

#### Exemplo

```bash
curl -X POST 'https://devprescription.amplimed.cloud/identity/v1/sessions/end' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{"embeddedToken":"eyJhbGciOiJIUzI1NiJ9.TOKEN_DA_SESSAO.ASSINATURA"}'
```

## Exemplo de ponta a ponta

As duas primeiras etapas encadeadas: o backend do parceiro autentica, recebe o token de parceiro e o usa imediatamente para criar a sessão. É nesta ligação que a maioria das integrações erra.

```bash
# 1. Autentique o parceiro e guarde o token.
#    As credenciais vêm do ambiente, nunca do código versionado.
TOKEN=$(curl -s -X POST 'https://devprescription.amplimed.cloud/identity/v1/partners/authenticate' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d "{\"clientId\": \"$PARTNER_CLIENT_ID\", \"secretKey\": \"$PARTNER_SECRET_KEY\"}" \
  | jq -r '.token')

# 2. Crie a sessão com esse token.
curl -X POST 'https://devprescription.amplimed.cloud/identity/v1/sessions' \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"professional":{"professionalId":null,"name":"Dra. Ana Souza","register":{"type":"CRM","value":"123456","state":"SC"},"specialty":"Clínica Médica","document":{"type":"cpf","value":"00000000000"}},"patient":{"patientId":null,"name":"João da Silva","document":{"type":"cpf","value":"11111111111"},"birthDate":"1985-04-12T00:00:00Z","gender":"male","phone":"+5548999999999","email":"joao@exemplo.com","address":{"street":"Rua das Flores","number":"100","complement":null,"neighborhood":"Centro","city":"Florianópolis","state":"SC","zipCode":"88010000","country":"BR"}},"customFields":[],"embeddedType":null}'
```

## Demais operações do contrato

Operações que o contrato publica e o passo a passo acima não usa.

### Sessões

#### Atualiza o patient da sessão de prescrição

`PATCH /sessions`

Recebe o embeddedToken (no corpo da requisição) e o patient com os
dados atualizados. Refaz o upsert do patient (incluindo
patient.address) e devolve um embeddedToken novo — o bundle precisa
substituir o token salvo pelo novo a partir dessa resposta. Sem token
de parceiro: a própria sessão encriptada é a autoridade, igual em
GET /v1/partners/sessions/validate.

**Corpo da requisição**

| Campo | Tipo | Obrigatório | Detalhes |
|---|---|---|---|
| `embeddedToken` | `string` | sim |  |
| `patient` | `object` | sim |  |
| `patient.patientId` | `string` | não | aceita `null` |
| `patient.name` | `string` | sim |  |
| `patient.document` | `object` | sim |  |
| `patient.document.type` | `string` | sim | valores: `cpf`, `passport`, `rg` |
| `patient.document.value` | `string` | sim |  |
| `patient.birthDate` | `string (date-time)` | não | aceita `null` |
| `patient.gender` | `string` | não | aceita `null` |
| `patient.phone` | `string` | não | aceita `null` |
| `patient.email` | `string (email)` | não | aceita `null` |
| `patient.address` | `object` | não | aceita `null` |
| `patient.address.street` | `string` | não |  |
| `patient.address.number` | `string` | não |  |
| `patient.address.complement` | `string` | não | aceita `null` |
| `patient.address.neighborhood` | `string` | não |  |
| `patient.address.city` | `string` | não |  |
| `patient.address.state` | `string` | não |  |
| `patient.address.zipCode` | `string` | não |  |
| `patient.address.country` | `string` | não |  |

**Resposta 200**

| Campo | Tipo | Obrigatório | Detalhes |
|---|---|---|---|
| `sessionId` | `string` | sim |  |
| `embeddedToken` | `string` | sim |  |
| `partner` | `object` | sim |  |
| `professional` | `object` | sim |  |
| `patient` | `object` | sim |  |
| `scopes` | `any[]` | sim |  |
| `featureFlags` | `any[]` | sim |  |
| `customFields` | `any[]` | sim |  |
| `status` | `string` | sim |  |
| `apiVersion` | `string` | sim |  |
| `expiresAt` | `string` | sim |  |

| Status | Significado |
|---|---|
| `404` | Nenhuma sessão encontrada para o embeddedToken informado. |
| `410` | Sessão encontrada mas não está mais ativa (expirada ou encerrada). |
| `422` | Payload inválido. |

## Como reagir a cada erro

Casado pelo campo `error_code` do corpo da resposta.

| `error_code` | O que fazer |
|---|---|
| `INVALID_PARTNER_CREDENTIALS` | O clientId ou a secretKey não conferem. Confira se você está usando as credenciais do ambiente certo — as de produção não funcionam aqui. Não repita a chamada em loop: o erro não se resolve sozinho. |

Quando o corpo não traz `error_code`, vale o status HTTP.

| Status | O que fazer |
|---|---|
| `401` | A API não reconheceu a credencial enviada. Verifique as credenciais e o ambiente. |
| `403` | A credencial é válida mas não tem permissão para esta operação. |
| `409` | Estado inesperado para esta sessão. Confira se ela ainda está ativa. |
| `410` | A sessão expirou ou foi encerrada. Crie uma nova sessão. |
| `422` | A API recusou o payload. A resposta traz o campo `errors` com o detalhe por campo. |
| `429` | Limite de requisições atingido. Espere antes de tentar de novo. |
| `500` | Erro interno da API. Se persistir, acione o time da Amplimed com o horário da chamada. |
