> ## Documentation Index
> Fetch the complete documentation index at: https://docs.d-api.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# API Oficial para SaaS (provedor)

> Ofereça a conexão do WhatsApp API Oficial dentro do seu próprio SaaS, usando a D-API como provedor.

Se você tem um SaaS e quer que **seus clientes** conectem o WhatsApp **API Oficial**
(Cloud API) sem sair do seu produto, a D-API funciona como **provedor**: você embute
um fluxo de conexão, cada número conectado vira uma conexão na **sua** conta D-API, e
você é cobrado **por conexão**. Você repassa como quiser para o seu cliente.

* **Você** é um usuário normal da D-API (sua API key secreta fica no seu backend).
* Seu cliente conecta o número dele; a conexão fica sob a sua conta, identificada por
  um `connectionId`.
* **Cobrança da Meta é do seu cliente** (ele usa o método de pagamento dele na WABA).
  A D-API só cobra a **taxa de plataforma por conexão** de você.

## Como funciona

O Embedded Signup da Meta **não roda no domínio do seu SaaS** (a Meta exige domínios
cadastrados). Por isso o fluxo roda numa **página hospedada pela D-API**
(`connect.d-api.cloud`), que você abre como **popup**. O resultado volta pro seu app
por `postMessage`. **Seu domínio não precisa ser cadastrado na Meta.**

<Steps>
  <Step title="Crie uma publishable key">
    Uma **publishable key** (`pk_live_…`) é pública de propósito: só inicia o
    onboarding — não envia mensagens nem lê dados. Crie no painel em
    **Integrações → Publishable Keys**, ou via API com a sua API key secreta:

    ```bash theme={null}
    curl -X POST https://api.d-api.cloud/api/v1/connections/cloud-api/publishable-keys \
      -H "Authorization: <SUA_API_KEY_SECRETA>" \
      -H "content-type: application/json" \
      -d '{}'
    ```

    A resposta traz o `key` **uma única vez** — guarde.
  </Step>

  <Step title="Abra o connect no seu frontend">
    Use o pacote `d-api-sdk` (entry `d-api-sdk/connect`):

    ```ts theme={null}
    import { DApiConnect } from "d-api-sdk/connect";

    const connect = new DApiConnect({ publishableKey: "pk_live_…" });

    const { connectionId, phoneNumber, accessToken } = await connect.start({
      webhookUrl: "https://seu-saas.com/hooks/dapi", // opcional
      webhookMode: "normalized",                     // opcional (padrão)
      metadata: { tenantId: "acme-42" },             // opcional, volta no webhook
    });
    ```

    Isso abre o popup hospedado, roda o Embedded Signup e resolve com o
    `connectionId` (e o [access token da Meta](#access-token-da-meta)). Também dá
    para fazer o handshake "na mão", em HTML puro — veja o
    [exemplo completo](https://github.com/d-api/exemplo-api-oficial-saas).
  </Step>

  <Step title="Associe o connectionId ao seu cliente">
    Guarde o `connectionId` retornado no seu banco, ligado ao cliente correspondente.
    A partir daí, esse número é uma conexão normal da D-API.
  </Step>

  <Step title="Envie mensagens pelo seu backend">
    Com a sua **API key secreta** (nunca no navegador), o `connectionId` é o
    `sessionId` nas rotas normais de envio:

    ```ts theme={null}
    import { DApi } from "d-api-sdk";

    const dapi = new DApi({ apiKey: "<SUA_API_KEY_SECRETA>" });
    await dapi.messages.sendText({ sessionId: connectionId, to, text });
    ```
  </Step>
</Steps>

## Coexistência (manter o app WhatsApp Business)

Por padrão o fluxo **cria/onboarda um número novo** na API Oficial. Se o seu cliente
quer **manter o app WhatsApp Business** no mesmo número (coexistência), passe
`mode: "coexistence"`:

```ts theme={null}
const { connectionId } = await connect.start({ mode: "coexistence" });
```

O fluxo é o mesmo do padrão — a janela vai direto para a Meta e volta com o
`connectionId`. A diferença é o que a Meta pede ao cliente: escanear um QR no app
WhatsApp Business. A sincronização do histórico começa automaticamente e pode
levar alguns minutos; oriente o cliente a manter o app aberto.

## Webhooks

Você define o webhook da conexão no `connect.start({ webhookUrl })`, e pode alterar
depois pela rota de atualização de sessão. Os eventos chegam com o id da
conexão/sessão no payload — então você pode apontar todas as conexões para um único
endpoint e distribuir pelo `connectionId`.

### O resultado chega por dois caminhos

Toda conexão criada pelo connect hospedado é anunciada **duas vezes**, sem opção
a configurar:

1. o `connect.start()` resolve no navegador com o `connectionId` e o access token;
2. um webhook [`session.created`](/whatsapp/webhooks/conexao) vai para o
   `webhookUrl` daquela conexão, com **todos** os dados — incluindo o access
   token e o seu `metadata`.

O segundo existe por causa do **mobile**. Em navegador de celular o popup abre
como uma aba, sem `window.opener` utilizável, então o `postMessage` que o
`start()` espera nunca chega: a promise pode rejeitar com "Conexão cancelada"
enquanto a conexão foi criada normalmente. Em webview de app nativo vale o mesmo.

<Warning>
  Em fluxo mobile, **não trate a rejeição do `start()` como falha** — confirme
  pelo `session.created` (ou pela rota de listagem de conexões) antes de mostrar
  erro para o seu cliente.
</Warning>

### Correlacionar com o seu cliente (`metadata`)

`metadata` é um JSON livre seu, guardado com a conexão e devolvido intacto em
`data.metadata` do `session.created`. É o que permite ligar a conexão ao tenant
sem depender do retorno síncrono:

```ts theme={null}
await connect.start({
  webhookUrl: "https://seu-saas.com/hooks/dapi",
  metadata: { tenantId: "acme-42", plano: "pro" },
});
```

Limite: **512 bytes** de JSON serializado. O valor viaja na URL do diálogo da
Meta, e acima disso o `start()` lança erro antes de abrir a janela. Guarde ali
identificadores, não objetos de negócio inteiros.

<Note>
  `metadata` é opcional e novo na versão **1.3.0** do SDK. Integrações em 1.2.0
  ou anterior seguem funcionando sem alteração — apenas recebem `metadata: {}`
  no evento.
</Note>

### Formato dos eventos (`webhookMode`)

Você escolhe em que **formato** os eventos chegam:

| `webhookMode`           | O que chega no seu endpoint                                                                                        |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `normalized` *(padrão)* | O evento **canônico da D-API** — o mesmo payload de uma conexão não-oficial, então um único handler serve as duas. |
| `meta_passthrough`      | O **corpo cru do webhook da Meta**, sem tradução — para quem já tem um parser do formato oficial.                  |

```ts theme={null}
const { connectionId } = await connect.start({
  webhookUrl: "https://seu-saas.com/hooks/dapi",
  webhookMode: "meta_passthrough", // padrão: "normalized"
});
```

Dá para trocar depois em
[Atualizar conexão](/api-reference/cloud-api/atualizar-conexao) (`webhookMode`).

## Access token da Meta

O resultado do `connect.start()` traz também o **access token da Meta** daquela
conexão, já descriptografado — útil se você quer chamar a Graph API direto (por
exemplo, para gerenciar templates ou o catálogo por fora da D-API):

```ts theme={null}
const {
  connectionId,
  accessToken,          // token da Graph API — SEGREDO
  accessTokenKind,      // "permanent" | "long_lived" | "short_lived" | "unknown"
  accessTokenLabel,     // resumo em português, pronto para exibir
  accessTokenExpiresAt, // ISO-8601, ou null quando não expira
} = await connect.start({ ... });
```

No Embedded Signup a Meta emite um token **permanente** (`accessTokenKind:
"permanent"`, `accessTokenExpiresAt: null`) — é o caso normal aqui. Os outros tipos
aparecem quando o token foi trocado manualmente: `long_lived` (\~60 dias) e
`short_lived` (\~1 hora).

<Warning>
  O `accessToken` é **segredo**: com ele dá para enviar mensagens e administrar a
  WABA do seu cliente. Encaminhe para o **seu backend** e pronto — não registre em
  log, não salve no `localStorage`, não exponha em URL.
</Warning>

Se preferir que o token **não trafegue pelo navegador**, ignore o campo e busque no
seu backend quando precisar, com a sua API key secreta:

```bash theme={null}
curl https://api.d-api.cloud/api/v1/connections/cloud-api/<CONNECTION_ID>/access-token \
  -H "Authorization: <SUA_API_KEY_SECRETA>"
```

A resposta traz o token e a mesma classificação — veja
[Obter access token da Meta](/api-reference/cloud-api/obter-access-token).

## Quando a conexão falha

Se o onboarding não conclui, a janela hospedada mostra o motivo **em português,
com o código embaixo** — um print dela já serve para o suporte. O mesmo motivo
volta para o seu app:

```js theme={null}
{ type: "dapi-connect-result", ok: false, error: "trial_required", errorLabel: "A conta D-API do parceiro precisa de uma assinatura ativa para criar conexões." }
```

Com o SDK, o `connect.start()` **rejeita** com essa mensagem. Se você fez o
handshake na mão, cheque o `ok` **antes** de ler o `data` — numa falha não existe
`data`, e ler `data.connectionId` direto faz você perder o motivo real:

```js theme={null}
if (!msg.ok) {
  throw Object.assign(new Error(msg.errorLabel || msg.error), { code: msg.error });
}
```

| `error`                               | O que aconteceu                                                 | O que fazer                       |
| ------------------------------------- | --------------------------------------------------------------- | --------------------------------- |
| `trial_required`                      | A **sua** conta D-API (a do parceiro) não tem assinatura ativa. | Ative o plano/teste na sua conta. |
| `number_in_use`                       | O número já está conectado em outra conta.                      | Desconecte antes de reconectar.   |
| `invalid_key`                         | Publishable key inválida, expirada ou revogada.                 | Gere outra.                       |
| `plan_not_ready`, `billing_not_ready` | Cobrança da sua conta ainda não configurada.                    | Finalize o cadastro de cobrança.  |
| `missing_code`, `access_denied`       | A Meta não devolveu o código — geralmente o cliente cancelou.   | Peça para repetir.                |
| `config_error`                        | Configuração do app Meta no servidor da D-API.                  | Falar com o suporte da D-API.     |
| `onboarding_failed`                   | A Meta não concluiu o cadastro do número.                       | Tentar novamente.                 |

<Note>
  `trial_required`, `plan_not_ready` e `billing_not_ready` são sobre a **sua**
  conta D-API, não a do seu cliente — é a taxa de plataforma por conexão. A
  cobrança das conversas continua sendo da conta Meta do cliente.
</Note>

## Cobrança (pass-through)

A cobrança das conversas pela Meta é do **seu cliente**: durante o Embedded Signup ele
usa o método de pagamento da conta Meta dele na WABA. A D-API **não** cobra as
conversas — só a **taxa de plataforma por conexão ativa**, na sua conta. Lembre o seu
cliente de finalizar o método de pagamento no WhatsApp Manager, se a Meta pedir.

## Segurança

* A **publishable key** é pública por design: escopo `onboard` apenas, com rate limit.
  O pior caso de vazamento é alguém iniciar um popup de Embedded Signup — sem acesso a
  dados nem envio.
* A sua **API key secreta** nunca vai para o navegador — só no seu backend.
* O resultado do onboarding só é entregue para a janela que abriu o popup, e o SDK
  valida que a mensagem veio do domínio hospedado da D-API.
* O **access token da Meta** volta pela página hospedada no *fragmento* da URL
  (`#…`), que o navegador nunca envia ao servidor — ele não aparece em log de acesso
  nem em `Referer`, e a página o apaga da barra de endereço depois de repassar. Daí
  em diante a responsabilidade é sua: mande para o backend e não persista no browser.

<Card title="Exemplo completo (HTML puro)" icon="github" href="https://github.com/d-api/exemplo-api-oficial-saas">
  Um único arquivo, sem framework, mostrando o fluxo popup + postMessage passo a passo.
</Card>
