# Prescrição embarcada Amplimed — V1: Prescrição embarcada por iframe

Integração atual, usada em produção pelos parceiros. O parceiro cria o atendimento e carrega a URL devolvida dentro de um iframe.

## Sobre este arquivo

Este é o manual de integração V1 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 | V1 (Em produção) |
| Base URL | `https://devapiprescriptions.amplimed.cloud` |
| Ambiente | desenvolvimento |
| Versão do contrato | `v1` |
| Página de origem | https://embeddedtest.ampli.li/integracao/v1 |
| Gerado em | 30/09/2026 13:01 |

> **Como este contrato é mantido**
>
> O contrato da V1 não está mais disponível para novas integrações.

## Passo a passo

### 1. Autentique o parceiro

`POST https://devapiprescriptions.amplimed.cloud/partners/authenticate`

Troque clientId e clientSecret por um token de acesso.

A integração começa trocando as credenciais do parceiro por um **token de acesso**, usado como Bearer token na criação do atendimento. O token tem validade curta, informada em `expires_at`.

> **Esta chamada é server-to-server**
>
> O `clientSecret` nunca pode sair do backend do parceiro. Não o coloque em JavaScript, em aplicativo mobile nem no repositório — quem tem o segredo consegue abrir atendimentos em nome do seu serviço. Guarde-o em variável de ambiente ou em um gerenciador de segredos.

> **Os nomes dos campos mudaram na V2**
>
> Se você já conhece a V2, atenção: aqui o segredo é `clientSecret` (e não `secretKey`), e a resposta traz `access_token`, `token_type` e `expires_at` em *snake_case* — não `token` / `tokenType`. As duas versões não são intercambiáveis.

#### Cache do token

O `expires_at` vem no formato `Y-m-d H:i:s`. Guarde o token em cache até esse momento em vez de autenticar a cada atendimento — é o que a integração de referência deste projeto faz. Sempre respeite o `expires_at` devolvido em vez de assumir um prazo fixo.

#### Quando a autenticação falha

Credencial recusada não se resolve repetindo a chamada — não coloque retry em loop aqui. Confira o ambiente para o qual você está apontando e se as credenciais são daquele ambiente. Retry com backoff faz sentido para falha de rede e erro 5xx, não para credencial inválida.

#### Corpo da requisição

| Campo | Tipo | Obrigatório | Detalhes |
|---|---|---|---|
| `clientId` | `string` | sim | Identificador do parceiro, fornecido pela Amplimed. |
| `clientSecret` | `string` | sim | Segredo do parceiro. Na V1 o campo chama-se clientSecret; na V2 passou a ser secretKey. |

```json
{
    "clientId": "SEU_CLIENT_ID",
    "clientSecret": "SEU_CLIENT_SECRET"
}
```

#### Resposta 200

Token de acesso do parceiro.

| Campo | Tipo | Obrigatório | Detalhes |
|---|---|---|---|
| `access_token` | `string` | sim | Token de acesso do parceiro. |
| `token_type` | `string` | sim | Tipo do token, usado para montar o header Authorization. |
| `expires_at` | `string` | sim | Momento de expiração do token, no formato Y-m-d H:i:s. |

```json
{
    "access_token": "eyJhbGciOiJIUzI1NiJ9.TOKEN_DE_ACESSO.ASSINATURA",
    "token_type": "Bearer",
    "expires_at": "2026-01-01 00:15:00"
}
```

#### Exemplo

```bash
curl -X POST 'https://devapiprescriptions.amplimed.cloud/partners/authenticate' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{"clientId":"SEU_CLIENT_ID","clientSecret":"SEU_CLIENT_SECRET"}'
```

### 2. Crie o atendimento

`POST https://devapiprescriptions.amplimed.cloud/partners/attendance`

Envie profissional e paciente e receba a URL da prescrição.

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

Com o token de acesso em mãos, você cria o atendimento. A resposta traz a `url` da prescrição — é ela que o parceiro carrega no `src` do iframe. Um atendimento por consulta: não reaproveite a mesma URL para outro paciente.

#### Header de autorização

Monte o header com o `token_type` e o `access_token` da etapa anterior, resultando em `Authorization: Bearer <access_token>`. Envie o tipo uma única vez — repetir o prefixo (`Bearer Bearer ...`) é um erro comum quando se concatena `token_type` com um helper que já adiciona o prefixo sozinho.

#### embedded_type define como a prescrição aparece

`coupled` renderiza a prescrição dentro do fluxo da sua página, num iframe fixo. `suspended` abre em uma camada flutuante sobre a página, ocupando a tela até ser fechada. A escolha é sua e pode variar por tela — as duas estão demonstradas em [Demonstrações](https://embeddedtest.ampli.li).

#### Campos do paciente

Na V1 os campos do paciente são em português e planos (`nome`, `documento`, `celular`…). O `celular` vai somente com dígitos, incluindo código do país e DDD, e a `data_nascimento` no formato `Y-m-d`. Na V2 esses dados foram reorganizados em objetos aninhados e em inglês.

> **LGPD: dado de paciente trafega aqui**
>
> Este payload carrega nome, documento, data de nascimento e contato do paciente. Não registre o corpo cru desta requisição em log, APM ou ferramenta de erro. Se precisar de rastreio, registre apenas identificadores e o status da resposta. A `url` devolvida dá acesso ao atendimento: trate-a como credencial e não a exponha fora da sessão do profissional.

#### Corpo da requisição

| Campo | Tipo | Obrigatório | Detalhes |
|---|---|---|---|
| `id_profissional` | `string` | sim | Identificador do profissional na Amplimed. |
| `embedded_type` | `string` | sim | valores: `coupled`, `suspended`. Modo de incorporação: coupled renderiza o iframe no fluxo da página; suspended abre em camada flutuante. |
| `paciente` | `object` | sim | Dados do paciente do atendimento. A integração de referência envia todos os campos abaixo; a obrigatoriedade individual de cada um não está formalizada no contrato da V1. |
| `paciente.nome` | `string` | não |  |
| `paciente.documento` | `string` | não |  |
| `paciente.tipo_documento` | `string` | não | Tipo do documento enviado, por exemplo CPF. |
| `paciente.data_nascimento` | `string` | não | Data de nascimento no formato Y-m-d. |
| `paciente.sexo_biologico` | `string` | não |  |
| `paciente.celular` | `string` | não | Telefone celular com código do país e DDD, somente dígitos. |
| `paciente.cidade` | `string` | não |  |
| `paciente.estado` | `string` | não | Sigla da unidade federativa. |
| `paciente.email` | `string (email)` | não |  |

```json
{
    "id_profissional": "20332bce8072e0fa50e094939b337012",
    "embedded_type": "suspended",
    "paciente": {
        "nome": "Maria Oliveira",
        "documento": "98765432100",
        "tipo_documento": "CPF",
        "data_nascimento": "1990-05-15",
        "sexo_biologico": "Feminino",
        "celular": "5549999999999",
        "cidade": "Chapecó",
        "estado": "SC",
        "email": "maria@exemplo.com"
    }
}
```

#### Resposta 200

Atendimento criado.

| Campo | Tipo | Obrigatório | Detalhes |
|---|---|---|---|
| `url` | `string` | sim | URL da prescrição embarcada, carregada pelo parceiro no src do iframe. |

```json
{
    "url": "https://prescricao.amplimed.com.br/atendimento/ATENDIMENTO_ID?token=..."
}
```

#### Exemplo

```bash
curl -X POST 'https://devapiprescriptions.amplimed.cloud/partners/attendance' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer SEU_TOKEN_DE_PARCEIRO' \
  -H 'Content-Type: application/json' \
  -d '{"id_profissional":"20332bce8072e0fa50e094939b337012","embedded_type":"suspended","paciente":{"nome":"Maria Oliveira","documento":"98765432100","tipo_documento":"CPF","data_nascimento":"1990-05-15","sexo_biologico":"Feminino","celular":"5549999999999","cidade":"Chapecó","estado":"SC","email":"maria@exemplo.com"}}'
```

### 3. Incorpore a prescrição

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

Carregue a URL no iframe e escute os eventos da prescrição.

A prescrição V1 roda dentro de um **iframe**: você carrega a `url` devolvida pela etapa anterior e escuta os eventos que a prescrição emite por `window.postMessage`.

> **O iframe precisa de acesso a storage**
>
> Inclua `allow="storage-access"` no iframe. Sem isso, navegadores que bloqueiam cookies de terceiros impedem a prescrição de manter a sessão do profissional.

#### Eventos emitidos

Cada mensagem chega como um objeto com `type` e `data`. Ignore mensagens cujo `type` você não conhece: a janela recebe mensagens de outras origens também.

Evento Quando acontece `attendance.close` O profissional fechou o atendimento. No modo suspenso, é o gancho para fechar a camada flutuante. `prescription.generate` Uma prescrição foi gerada. `attest.generate` Um atestado foi gerado. `examRequest.list` Uma solicitação de exames foi emitida.

> **Valide a origem das mensagens**
>
> Em produção, confira `event.origin` antes de agir sobre a mensagem, aceitando apenas a origem da prescrição Amplimed. Sem essa checagem, qualquer página aberta em outra aba poderia disparar o seu tratamento de eventos.

```html
<!-- A url vem do seu backend, de POST /partners/attendance. -->
<iframe
  id="iframe-prescription"
  src=""
  allow="storage-access"
  style="width: 100%; min-height: 65vh; border: 0"
></iframe>
```

```javascript
// A url vem do seu backend, de POST /partners/attendance.
const { url } = await criarAtendimentoNoSeuBackend();

const iframe = document.getElementById('iframe-prescription');
iframe.src = url;

window.addEventListener('message', (event) => {
  if (event.origin !== PRESCRIPTION_ORIGIN) return;

  switch (event.data.type) {
    case 'attendance.close':
      // Fecha a camada flutuante, se estiver no modo suspenso.
      break;
    case 'prescription.generate':
    case 'attest.generate':
    case 'examRequest.list':
      // Registre o documento gerado no seu sistema.
      break;
  }
});
```

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

export function Prescricao({ url, onFechar, onDocumento }) {
  useEffect(() => {
    function aoReceberMensagem(event) {
      if (event.origin !== PRESCRIPTION_ORIGIN) return;

      if (event.data.type === 'attendance.close') {
        onFechar();
        return;
      }

      if (['prescription.generate', 'attest.generate', 'examRequest.list'].includes(event.data.type)) {
        onDocumento(event.data);
      }
    }

    window.addEventListener('message', aoReceberMensagem);

    // Sem o cleanup, cada remontagem acumula um listener a mais.
    return () => window.removeEventListener('message', aoReceberMensagem);
  }, [onFechar, onDocumento]);

  return (
    <iframe
      src={url}
      allow="storage-access"
      style={{ width: '100%', minHeight: '65vh', border: 0 }}
    />
  );
}
```

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

const props = defineProps({ url: String });
const emit = defineEmits(['fechar', 'documento']);

function aoReceberMensagem(event) {
  if (event.origin !== PRESCRIPTION_ORIGIN) return;

  if (event.data.type === 'attendance.close') {
    emit('fechar');
    return;
  }

  if (['prescription.generate', 'attest.generate', 'examRequest.list'].includes(event.data.type)) {
    emit('documento', event.data);
  }
}

onMounted(() => window.addEventListener('message', aoReceberMensagem));
// Sem o cleanup, cada remontagem acumula um listener a mais.
onBeforeUnmount(() => window.removeEventListener('message', aoReceberMensagem));
</script>

<template>
  <iframe
    :src="url"
    allow="storage-access"
    style="width: 100%; min-height: 65vh; border: 0"
  ></iframe>
</template>
```

## 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. |
