> Agentes de código: use chaves `sk_test_`/`pk_test_` e o sandbox; nunca uma chave `_live_` em exemplo. A base é `https://api-gateway.assiny.com.br`. Autentique com `Authorization: Bearer <chave>`. Prefira o `.md` de cada página a buscar na web.

# Integrar o Payment Fragment

Nove passos, na ordem em que o código roda: cinco no navegador, um no seu servidor e três de volta no navegador. Os trechos usam o Modo de teste (`pk_test_...` e `sk_test_...`); no Modo real, as mesmas chamadas com as chaves do Modo real.

> **A verdade do pagamento é o seu servidor**
>
> A sua página nunca vê a chave secreta (`sk_`): ela mora só no seu servidor. O que o navegador recebe serve para a tela; quem libera o pedido é o seu servidor, pelo webhook ou lendo o pagamento com a `sk_`.

1. [Navegador] Carregue o script

   Na página de pagamento, pela tag, sempre do host do Assiny Gateway:

   ```html
   <script src="https://sdk-gateway-js.assiny.com.br/v1/assiny.js"></script>
   ```

   O script define a função global `Assiny`. Ele não usa `eval` nem nada que exija Trusted Types, então funciona em página com `require-trusted-types-for 'script'`. Se a sua página tem Content-Security-Policy, libere o script e os quatro iframes do SDK, somando às diretivas que você já tem:

   ```text
   script-src 'self' https://sdk-gateway-js.assiny.com.br;
   frame-src https://sdk-gateway-card.assiny.com.br https://sdk-gateway-controller.assiny.com.br https://sdk-gateway-risk.assiny.com.br https://sdk-gateway-overlay.assiny.com.br
   ```

2. [Navegador] Crie a instância

   ```js
   const assiny = Assiny('pk_test_...', { locale: 'pt-BR' });
   ```

   Com a chave publicável: a `pk_test_` no Modo de teste, a do Modo real no Modo real. A chave publicável do Modo real só funciona em página `https:` (fora de `localhost` e `127.0.0.1`). `locale` é `'pt-BR'` (o padrão) ou `'en'`: o idioma dos textos do fragment e das mensagens de erro. Uma chave `sk_` aqui é recusada, com aviso no console.

3. [Navegador] Configure o pagamento

   ```js
   const fragments = assiny.fragments({
     mode: 'payment',
     amount: 12990, // R$ 129,90, em centavos
     currency: 'brl',
     appearance: { theme: 'assiny' },
   });
   ```

   `amount` é o total em centavos, um inteiro maior que zero. Ele serve para desenhar e dividir o pagamento; o valor que vale é o que o seu servidor manda no passo 6. Se o total mudar antes do clique (cupom, frete), chame `fragments.update({ amount })`. A `appearance` entra só aqui: [Aparência](/fragments/aparencia.md).

4. [Navegador] Monte o fragment

   ```html
   <div id="pagamento"></div>
   ```

   ```js
   const payment = fragments.create('payment', {
     paymentMethodTypes: ['card', 'pix'],
     combine: 'auto',
   });
   payment.on('loaderror', event => console.error(event.error.code));
   payment.mount('#pagamento');
   ```

   O seletor precisa casar com exatamente um elemento. O fragment ocupa a largura dele e a altura acompanha o conteúdo. Opções e eventos: [AssinyGateway Payment Fragment](/fragments/payment-fragment.md).

5. [Navegador] Crie os métodos de pagamento no clique

   ```js
   pagar.addEventListener('click', async () => {
     const { parts, error } = await assiny.createPaymentMethod({
       fragments,
       params: {
         billing_details: {
           name: 'Maria Souza',
           email: 'maria@example.com',
           address: { country: 'BR' },
         },
       },
     });

     if (error) {
       // A message já vem no idioma do locale; error.part diz qual Parte falhou.
       mostrarErro(error.message);
       return;
     }

     const resposta = await fetch('/checkout', {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({ parts }),
     });
     const pagamento = await resposta.json();
     // Passos 7 e 8.
   });
   ```

   O fragment confere os campos e cria um método de pagamento (`pm_`) por Parte: uma no cartão ou no PIX, duas no Combinar. A promessa resolve `{ parts: [{ method, amount, payment_method }] }`, na ordem das Partes, ou `{ error }`; ela nunca rejeita. Mande `parts` ao seu servidor (aqui, a rota `/checkout` é da sua loja).

   O que você manda em `billing_details` vale como veio, para todas as Partes; o que falta entra dos campos Nome no cartão e País do fragment, quando ligados e preenchidos. Chaves aceitas: `name`, `email`, `phone`, `tax_id` com `tax_id_type` e `address` (`line1`, `line2`, `city`, `state`, `postal_code`, `country`); outra chave é descartada com aviso no console. Na política padrão, o pagador precisa de `email` no cartão e no PIX e de `name` no cartão; sem eles, a criação do método de pagamento ou o confirm recusa com `payer_field_missing`.

6. [Servidor] Crie e confirme o pagamento no servidor

   Com a chave secreta, dois pedidos à API:

   - `POST https://api-gateway.assiny.com.br/v1/payment_intents`, com `Authorization: Bearer sk_test_...`, `Content-Type: application/json` e o corpo:
     - `amount`: o total do pedido em centavos, calculado no seu servidor; a soma das Partes tem de ser igual a ele.
     - `currency`: `'brl'`.
     - `billing_details`: o Comprador (`name`, `email`, `tax_id` com `tax_id_type`, `phone`, `address`).
     - `parts`: as Partes que vieram do navegador, cada uma `{ method, amount, payment_method }`.
     - `combined_payment_methods: { enabled: true }` (opcional, recomendado com duas Partes): liga a conferência de que a política da Conta aceita o Combinar na moeda.
     - `payment_method_options: { pix: { expires_after_seconds } }` (opcional): o prazo do PIX em segundos; no Combinar (cartão + PIX), vale o prazo da política da Conta para o Combinar, não o enviado ([PIX e a tela do QR](/fragments/pix.md)).
     - `customer` (opcional): o `cus_` do Comprador.
   - `POST https://api-gateway.assiny.com.br/v1/payment_intents/{id}/confirm`, com o `id` que voltou e sem corpo. O confirm não tem parâmetro: qualquer chave no corpo é 400 `parameter_invalid`.

   Devolva ao navegador só `id`, `status`, `client_secret` e `last_payment_error`.

   #### curl

   ```bash
   # 1. Crie o pagamento com as Partes que o navegador mandou.
   curl https://api-gateway.assiny.com.br/v1/payment_intents \
     -H "Authorization: Bearer sk_test_..." \
     -H "Content-Type: application/json" \
     -d '{
       "amount": 12990,
       "currency": "brl",
       "billing_details": { "name": "Maria Souza", "email": "maria@example.com" },
       "parts": [
         { "method": "card", "amount": 12990, "payment_method": "pm_..." }
       ]
     }'

   # 2. Confirme com o id que voltou, sem corpo.
   curl -X POST https://api-gateway.assiny.com.br/v1/payment_intents/pi_.../confirm \
     -H "Authorization: Bearer sk_test_..."
   ```

   #### Node

   ```js
   const API_BASE = "https://api-gateway.assiny.com.br";
   const SECRET_KEY = "sk_test_..."; // a sua chave secreta, só no servidor

   // Chama a API com a chave secreta e devolve o status e o JSON da resposta.
   async function post(path, body) {
     const headers = { Authorization: `Bearer ${SECRET_KEY}` };
     if (body !== undefined) headers["Content-Type"] = "application/json";
     const response = await fetch(API_BASE + path, {
       method: "POST",
       headers,
       body: body === undefined ? undefined : JSON.stringify(body),
     });
     return [response.status, await response.json()];
   }

   // amount: o total do pedido em centavos, calculado no seu servidor.
   // parts: o `parts` que o navegador recebeu de createPaymentMethod.
   // billingDetails: o Comprador, por exemplo { name, email }.
   // Devolve o status HTTP e o corpo para responder ao navegador.
   async function checkout(amount, parts, billingDetails) {
     const body = {
       amount,
       currency: "brl",
       billing_details: billingDetails,
       parts: parts.map(part => ({
         method: part.method,
         amount: part.amount,
         payment_method: part.payment_method,
       })),
     };
     // Duas Partes: o Comprador combinou métodos.
     if (parts.length === 2) body.combined_payment_methods = { enabled: true };

     let [status, intent] = await post("/v1/payment_intents", body);
     if (status >= 300) return [status, intent];

     [status, intent] = await post(`/v1/payment_intents/${encodeURIComponent(intent.id)}/confirm`);
     if (status >= 300) return [status, intent];

     return [200, {
       id: intent.id,
       status: intent.status,
       client_secret: intent.client_secret,
       last_payment_error: intent.last_payment_error,
     }];
   }
   ```

   #### Go

   ```go
   package checkout

   import (
   	"bytes"
   	"encoding/json"
   	"io"
   	"net/http"
   	"net/url"
   )

   const (
   	apiBase   = "https://api-gateway.assiny.com.br"
   	secretKey = "sk_test_..." // a sua chave secreta, só no servidor
   )

   // Part é um item do parts que o navegador recebeu de createPaymentMethod.
   type Part struct {
   	Method        string `json:"method"`
   	Amount        int64  `json:"amount"`
   	PaymentMethod string `json:"payment_method"`
   }

   // post chama a API com a chave secreta e devolve o status e o JSON da resposta.
   func post(path string, body any) (int, map[string]any, error) {
   	var reader io.Reader
   	if body != nil {
   		encoded, err := json.Marshal(body)
   		if err != nil {
   			return 0, nil, err
   		}
   		reader = bytes.NewReader(encoded)
   	}
   	request, err := http.NewRequest(http.MethodPost, apiBase+path, reader)
   	if err != nil {
   		return 0, nil, err
   	}
   	request.Header.Set("Authorization", "Bearer "+secretKey)
   	if body != nil {
   		request.Header.Set("Content-Type", "application/json")
   	}
   	response, err := http.DefaultClient.Do(request)
   	if err != nil {
   		return 0, nil, err
   	}
   	defer response.Body.Close()
   	var decoded map[string]any
   	if err := json.NewDecoder(response.Body).Decode(&decoded); err != nil {
   		return 0, nil, err
   	}
   	return response.StatusCode, decoded, nil
   }

   // checkout cria e confirma o pagamento e devolve o status HTTP e o corpo para o navegador.
   // amount é o total do pedido em centavos, calculado no seu servidor; billingDetails é o
   // Comprador, por exemplo {"name": ..., "email": ...}.
   func checkout(amount int64, parts []Part, billingDetails map[string]any) (int, map[string]any, error) {
   	body := map[string]any{
   		"amount":          amount,
   		"currency":        "brl",
   		"billing_details": billingDetails,
   		"parts":           parts,
   	}
   	// Duas Partes: o Comprador combinou métodos.
   	if len(parts) == 2 {
   		body["combined_payment_methods"] = map[string]any{"enabled": true}
   	}

   	status, intent, err := post("/v1/payment_intents", body)
   	if err != nil || status >= 300 {
   		return status, intent, err
   	}

   	id, _ := intent["id"].(string)
   	status, intent, err = post("/v1/payment_intents/"+url.PathEscape(id)+"/confirm", nil)
   	if err != nil || status >= 300 {
   		return status, intent, err
   	}

   	return http.StatusOK, map[string]any{
   		"id":                 intent["id"],
   		"status":             intent["status"],
   		"client_secret":      intent["client_secret"],
   		"last_payment_error": intent["last_payment_error"],
   	}, nil
   }
   ```

   #### Ruby

   ```ruby
   require "json"
   require "net/http"
   require "uri"

   API_BASE = "https://api-gateway.assiny.com.br"
   SECRET_KEY = "sk_test_..." # a sua chave secreta, só no servidor

   # Chama a API com a chave secreta e devolve o status e o JSON da resposta.
   def post(path, body = nil)
     uri = URI(API_BASE + path)
     request = Net::HTTP::Post.new(uri)
     request["Authorization"] = "Bearer #{SECRET_KEY}"
     if body
       request["Content-Type"] = "application/json"
       request.body = JSON.generate(body)
     end
     response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
       http.request(request)
     end
     [response.code.to_i, JSON.parse(response.body)]
   end

   # amount: o total do pedido em centavos, calculado no seu servidor.
   # parts: o parts que o navegador recebeu de createPaymentMethod, lido do JSON.
   # billing_details: o Comprador, por exemplo { "name" => ..., "email" => ... }.
   # Devolve o status HTTP e o corpo para responder ao navegador.
   def checkout(amount, parts, billing_details)
     body = {
       "amount" => amount,
       "currency" => "brl",
       "billing_details" => billing_details,
       "parts" => parts.map { |part| part.slice("method", "amount", "payment_method") }
     }
     # Duas Partes: o Comprador combinou métodos.
     body["combined_payment_methods"] = { "enabled" => true } if parts.length == 2

     status, intent = post("/v1/payment_intents", body)
     return [status, intent] if status >= 300

     status, intent = post("/v1/payment_intents/#{URI.encode_www_form_component(intent["id"])}/confirm")
     return [status, intent] if status >= 300

     [200, intent.slice("id", "status", "client_secret", "last_payment_error")]
   end
   ```

   #### Python

   ```python
   import json
   import urllib.error
   import urllib.parse
   import urllib.request

   API_BASE = "https://api-gateway.assiny.com.br"
   SECRET_KEY = "sk_test_..."  # a sua chave secreta, só no servidor

   def post(path, body=None):
       """Chama a API com a chave secreta e devolve o status e o JSON da resposta."""
       headers = {"Authorization": "Bearer " + SECRET_KEY}
       data = None
       if body is not None:
           headers["Content-Type"] = "application/json"
           data = json.dumps(body).encode("utf-8")
       request = urllib.request.Request(API_BASE + path, data=data, headers=headers, method="POST")
       try:
           with urllib.request.urlopen(request) as response:
               return response.status, json.load(response)
       except urllib.error.HTTPError as error:
           return error.code, json.load(error)

   def checkout(amount, parts, billing_details):
       """Cria e confirma o pagamento e devolve o status HTTP e o corpo para o navegador.

       amount: o total do pedido em centavos, calculado no seu servidor.
       parts: o parts que o navegador recebeu de createPaymentMethod, lido do JSON.
       billing_details: o Comprador, por exemplo {"name": ..., "email": ...}.
       """
       body = {
           "amount": amount,
           "currency": "brl",
           "billing_details": billing_details,
           "parts": [
               {key: part[key] for key in ("method", "amount", "payment_method")}
               for part in parts
           ],
       }
       # Duas Partes: o Comprador combinou métodos.
       if len(parts) == 2:
           body["combined_payment_methods"] = {"enabled": True}

       status, intent = post("/v1/payment_intents", body)
       if status >= 300:
           return status, intent

       status, intent = post("/v1/payment_intents/" + urllib.parse.quote(intent["id"], safe="") + "/confirm")
       if status >= 300:
           return status, intent

       keys = ("id", "status", "client_secret", "last_payment_error")
       return 200, {key: intent.get(key) for key in keys}
   ```

   No curl, os dois pedidos à mão, com um `pm_` e um `pi_` de exemplo; nas outras linguagens, uma função `checkout` que recebe as Partes, faz os dois pedidos e devolve o status HTTP e o corpo para responder ao navegador.

   Uma recusa volta com o status HTTP e o corpo `{ error: { type, code, message, param, request_id } }` (`param` só quando a recusa é de um campo), e os trechos a devolvem como veio: 401 `api_key_invalid` (chave ausente ou inválida), 403 `permission_denied`, 400 com o motivo em `code`, como `parts_sum_mismatch` (a soma das Partes diferente de `amount`).

7. [Navegador] Siga o próximo passo

   ```js
   let resultado = pagamento;

   if (pagamento.status === 'requires_action' || pagamento.status === 'processing') {
     const { paymentIntent, error } = await assiny.handleNextAction({
       clientSecret: pagamento.client_secret,
     });
     if (error) {
       mostrarErro(error.message);
       return;
     }
     resultado = paymentIntent;
   }
   ```

   Com `requires_action` (o PIX), a tela do QR do PIX abre em tela cheia sobre a sua página e o SDK lê o pagamento a cada 3 s até o fim; com `processing`, o SDK só lê, sem desenhar nada. A promessa resolve `{ paymentIntent }` com `status`, `parts` e `last_payment_error`, ou `{ error }`. O detalhe está em [PIX e a tela do QR](/fragments/pix.md).

8. [Navegador] Trate o resultado

   ```js
   switch (resultado.status) {
     case 'succeeded':
       mostrarConfirmacao();
       break;
     case 'requires_payment_method':
       mostrarErro(resultado.last_payment_error?.message);
       break;
     case 'requires_action':
       // O Comprador fechou a tela do QR; handleNextAction de novo a reabre.
       break;
     default:
       // canceled ou requires_confirmation.
       mostrarErro('Não foi possível concluir o pagamento.');
   }
   ```

   - `succeeded`: pago. Mostre a confirmação; o pedido é liberado pelo seu servidor.
   - `requires_payment_method`: o cartão foi recusado ou o PIX venceu; `last_payment_error` diz o motivo. Para tentar de novo, repita a partir do passo 5.
   - `requires_action` depois do `handleNextAction`: o Comprador fechou a tela do QR (Esc ou Fechar) sem pagar; chamar `handleNextAction` de novo reabre o mesmo QR.
   - `canceled`: o pagamento foi cancelado.
   - `requires_confirmation`: o pagamento foi criado e ainda não confirmado (passo 6).
   - `{ error }` em qualquer passo: `error.message` no idioma do `locale`. A lista completa está na [Referência do SDK](/fragments/referencia.md).

9. [Navegador] Teste no Modo de teste

   Com as chaves `pk_test_...` e `sk_test_...`, nada é cobrado. Use uma validade no futuro e o CVC com o número de dígitos da bandeira (4 na Amex, 3 nas demais).

   Cartões aprovados:

   | Número | Bandeira |
   |---|---|
   | `4242 4242 4242 4242` | Visa |
   | `5555 5555 5555 4444` | Mastercard |
   | `4000 0007 6000 0002` | Visa (Brasil) |
   | `3782 822463 10005` | Amex |

   Cartões recusados, com o motivo que o Modo de teste simula e o `decline_code` que volta:

   | Número | Motivo | `decline_code` |
   |---|---|---|
   | `4000 0000 0000 0002` | `generic_decline` | `05` |
   | `4000 0000 0000 9995` | `insufficient_funds` | `51` |
   | `4000 0000 0000 9987` | `lost_card` | `41` |
   | `4000 0000 0000 9979` | `stolen_card` | `43` |
   | `4000 0000 0000 0069` | `expired_card` | `54` |
   | `4000 0000 0000 0127` | `incorrect_cvc` | `835` |
   | `4000 0000 0000 0119` | `processing_error` | `96` |
   | `4000 0000 0000 6975` | `card_velocity_exceeded` | `61` |

   Na recusa, o confirm responde `status: 'requires_payment_method'` e `last_payment_error` com `type: 'card_error'`, `code: 'card_declined'` e o `decline_code` da tabela; o motivo é só a descrição do que o Modo de teste simula e não volta na resposta. Para forçar a recusa com qualquer cartão, ponha `REFUSED<código>` no nome do portador (o `billing_details.name` ou o campo Nome no cartão), como `Maria REFUSED51`: o código volta em `last_payment_error.decline_code`; um código que o Modo de teste não conhece é ignorado.

   PIX: na faixa de valor que a política da sua Conta aceita (na política padrão, a partir de R$ 3,50), a Parte PIX de teste é sempre paga sozinha em cerca de 5 s, e a tela do QR mostra o pagamento confirmado.
