Pular para o conteúdo principal

Integração com o Orkestral Payment SDK

O @orkestralpay/payment-sdk permite que o merchant capture dados de cartão de crédito utilizando Hosted Fields.

Os campos de número do cartão, validade e CVV são carregados dentro de iframes seguros. Dessa forma, os dados sensíveis do cartão não ficam disponíveis no HTML, no JavaScript ou no backend do merchant.

Ao finalizar o preenchimento, o SDK realiza a tokenização dos dados e retorna somente um token. Esse token deve ser enviado ao backend do merchant para que o pagamento seja criado.

:::info Responsabilidade do SDK O SDK é responsável pela captura segura e pela tokenização dos dados do cartão. A criação do pagamento deve ser realizada pelo backend do merchant utilizando a API da Orkestral. :::

Fluxo da integração

Cliente

│ Preenche os Hosted Fields

Frontend do merchant

│ card.tokenize()

Hosted Fields da Orkestral

│ Tokeniza os dados do cartão

Frontend recebe o token

│ Envia somente o token

Backend do merchant

│ Solicita a criação do pagamento

API de pagamentos da Orkestral

Responsabilidades do frontend

O frontend do merchant deve:

  1. Instalar e inicializar o SDK;
  2. Criar os containers dos campos de cartão;
  3. Acompanhar os eventos de preenchimento e validação;
  4. Solicitar a tokenização dos dados;
  5. Enviar o token ao backend do merchant.

Responsabilidades do backend

O backend do merchant deve:

  1. Receber o token gerado pelo SDK;
  2. Validar os dados da compra;
  3. Recuperar o valor real do pedido;
  4. Autenticar-se na API da Orkestral;
  5. Criar o pagamento utilizando o token;
  6. Retornar o resultado ao frontend.

:::warning Validação do valor O backend não deve confiar no valor enviado pelo navegador. O valor, a moeda e os dados do pedido devem ser recuperados ou validados no servidor. :::

Pré-requisitos

Antes de iniciar a integração, o merchant deve receber da Orkestral:

  • Chave pública do merchant;
  • URL dos Hosted Fields;
  • Caminhos dos campos de cartão;
  • Credenciais de autenticação do backend;
  • URL da API de pagamentos;
  • Documentação do endpoint de criação de pagamentos.

A chave utilizada no frontend deve ser uma chave pública.

Credenciais privadas, secrets ou tokens de autenticação da API nunca devem ser expostos no navegador.

Instalação

NPM

npm install @orkestralpay/payment-sdk

CDN

<script src="https://unpkg.com/@orkestralpay/payment-sdk/dist/umd/payment-sdk.min.js"></script>

Estrutura do formulário

Crie um container para cada campo seguro do cartão.

<form id="payment-form">
<div class="form-group">
<label for="card-number">Número do cartão</label>
<div id="card-number" class="hosted-field"></div>
<span id="card-number-error" class="field-error"></span>
</div>

<div class="form-row">
<div class="form-group">
<label for="card-expiry">Validade</label>
<div id="card-expiry" class="hosted-field"></div>
<span id="card-expiry-error" class="field-error"></span>
</div>

<div class="form-group">
<label for="card-cvv">CVV</label>
<div id="card-cvv" class="hosted-field"></div>
<span id="card-cvv-error" class="field-error"></span>
</div>
</div>

<button id="pay-button" type="submit" disabled>
Finalizar pagamento
</button>

<div id="payment-message"></div>
</form>

Os containers devem possuir altura definida, pois os iframes criados pelo SDK ocupam toda a largura e altura do elemento pai.

.hosted-field {
height: 48px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: #ffffff;
}

.hosted-field.is-focused {
border-color: #2563eb;
}

.hosted-field.is-invalid {
border-color: #dc2626;
}

.field-error {
display: block;
min-height: 18px;
margin-top: 4px;
font-size: 12px;
color: #dc2626;
}

.form-row {
display: flex;
gap: 16px;
}

.form-group {
flex: 1;
margin-bottom: 16px;
}

Inicialização do SDK

import { PaymentSDK } from '@orkestralpay/payment-sdk';

const sdk = PaymentSDK.init({
publicKey: 'SUA_CHAVE_PUBLICA',
hostedFieldsUrl: 'URL_DOS_HOSTED_FIELDS',

fieldPaths: {
cardNumber: '/card-number',
expiry: '/expiry',
cvv: '/cvv',
},

tokenizeTimeout: 30000,
});

Parâmetros

ParâmetroObrigatórioDescrição
publicKeySimChave pública utilizada para identificar o merchant
hostedFieldsUrlSimURL base do servidor responsável pelos Hosted Fields
fieldPaths.cardNumberSimCaminho do iframe do número do cartão
fieldPaths.expirySimCaminho do iframe da validade
fieldPaths.cvvSimCaminho do iframe do CVV
sessionIdNãoIdentificador da sessão de pagamento
tokenizeTimeoutNãoTempo máximo da tokenização em milissegundos

O identificador da sessão pode ser consultado por meio do método getSessionId().

const sessionId = sdk.getSessionId();

console.log('Payment session:', sessionId);

Criação dos campos de cartão

Após inicializar o SDK, informe os elementos em que os campos seguros serão renderizados.

const card = sdk.createCreditCard({
cardNumber: {
selector: '#card-number',
placeholder: '0000 0000 0000 0000',
},

expiry: {
selector: '#card-expiry',
placeholder: 'MM/AA',
},

cvv: {
selector: '#card-cvv',
placeholder: '123',
},

styles: {
fontSize: '16px',
fontFamily: 'Inter, Arial, sans-serif',
color: '#111827',
backgroundColor: 'transparent',
padding: '12px',
},
});

Os containers devem existir no DOM antes da execução de createCreditCard().

Estilos disponíveis

const styles = {
backgroundColor: '#ffffff',
color: '#111827',
fontFamily: 'Inter, sans-serif',
fontSize: '16px',
fontStyle: 'normal',
fontWeight: '400',
letterSpacing: '0.5px',
lineHeight: '1.5',
padding: '12px',
textAlign: 'left',
textTransform: 'none',
};

Também é possível sobrescrever estilos de um campo específico.

const card = sdk.createCreditCard({
cardNumber: {
selector: '#card-number',
placeholder: '0000 0000 0000 0000',
styles: {
letterSpacing: '2px',
},
},

expiry: {
selector: '#card-expiry',
placeholder: 'MM/AA',
},

cvv: {
selector: '#card-cvv',
placeholder: '123',
},

styles: {
fontSize: '16px',
color: '#111827',
},
});

Eventos dos campos

O SDK disponibiliza eventos para controlar o estado visual e a validação do formulário.

Ready

A tokenização deve ser executada somente depois que todos os campos estiverem prontos.

const readyFields = new Set<string>();

card.on('ready', ({ field }) => {
readyFields.add(field);

if (readyFields.size === 3) {
console.log('Todos os campos estão prontos.');
}
});

Focus e blur

card.on('focus', ({ field }) => {
getFieldContainer(field)?.classList.add('is-focused');
});

card.on('blur', ({ field }) => {
getFieldContainer(field)?.classList.remove('is-focused');
});

Change

const completedFields = {
cardNumber: false,
expiry: false,
cvv: false,
};

const payButton = document.querySelector<HTMLButtonElement>('#pay-button');

card.on('change', ({ field, complete }) => {
completedFields[field] = complete;

const formIsComplete = Object.values(completedFields).every(Boolean);

if (payButton) {
payButton.disabled = !formIsComplete;
}
});

O evento change informa:

PropriedadeDescrição
fieldCampo alterado
emptyIndica se o campo está vazio
completeIndica se o preenchimento está completo

Validation

card.on('validation', ({ field, valid, error }) => {
const container = getFieldContainer(field);
const errorElement = getErrorElement(field);

container?.classList.toggle('is-invalid', !valid);

if (errorElement) {
errorElement.textContent = valid ? '' : error ?? 'Campo inválido.';
}
});

Error

card.on('error', ({ field, code, message }) => {
console.error('Hosted Fields error:', {
field,
code,
message,
});
});

Funções auxiliares

function getFieldContainer(field: string): HTMLElement | null {
const containers: Record<string, string> = {
cardNumber: '#card-number',
expiry: '#card-expiry',
cvv: '#card-cvv',
};

return document.querySelector(containers[field]);
}

function getErrorElement(field: string): HTMLElement | null {
const elements: Record<string, string> = {
cardNumber: '#card-number-error',
expiry: '#card-expiry-error',
cvv: '#card-cvv-error',
};

return document.querySelector(elements[field]);
}

Tokenização do cartão

No envio do formulário, chame o método card.tokenize().

const paymentForm = document.querySelector<HTMLFormElement>('#payment-form');
const paymentMessage = document.querySelector<HTMLElement>('#payment-message');

paymentForm?.addEventListener('submit', async (event) => {
event.preventDefault();

if (!payButton) {
return;
}

payButton.disabled = true;
payButton.textContent = 'Processando...';

try {
const tokenizationResult = await card.tokenize({
saveCard: false,

customerName: 'João da Silva',
customerDocument: '12345678900',

billingAddress: {
street: 'Avenida Paulista, 1000',
city: 'São Paulo',
state: 'SP',
postalCode: '01310100',
country: 'BR',
},
});

if (!tokenizationResult.success) {
showTokenizationError(tokenizationResult.error);
return;
}

const { token, cardBrand, lastFourDigits } = tokenizationResult.data;

console.log('Token criado:', token);
console.log('Bandeira:', cardBrand);
console.log('Últimos dígitos:', lastFourDigits);

await createPayment({ token });
} catch (error) {
console.error('Unexpected payment error:', error);

if (paymentMessage) {
paymentMessage.textContent =
'Não foi possível processar o pagamento. Tente novamente.';
}
} finally {
payButton.disabled = false;
payButton.textContent = 'Finalizar pagamento';
}
});

Parâmetros da tokenização

PropriedadeObrigatórioDescrição
saveCardNãoIndica se o cartão deve ser salvo
customerIdCondicionalObrigatório quando saveCard for true
customerNameNãoNome completo do titular
customerDocumentNãoDocumento do titular
billingAddress.streetNãoEndereço
billingAddress.cityNãoCidade
billingAddress.stateNãoEstado ou região
billingAddress.postalCodeNãoCEP
billingAddress.countryNãoCódigo ISO do país

Resposta da tokenização

interface TokenizationSuccess {
success: true;
data: {
token: string;
cardBrand?: string;
lastFourDigits?: string;
expiryMonth?: string;
expiryYear?: string;
cardSaved?: boolean;
vaultId?: string;
};
}

interface TokenizationFailure {
success: false;
error: {
code: string;
message: string;
field?: 'cardNumber' | 'expiry' | 'cvv';
details?: Record<string, unknown>;
};
}

type TokenizeResponse = TokenizationSuccess | TokenizationFailure;

Envio do token ao backend

Depois de receber o token, o frontend deve enviá-lo ao backend do próprio merchant.

interface CreatePaymentInput {
token: string;
}

async function createPayment({ token }: CreatePaymentInput): Promise<void> {
const response = await fetch('/api/payments', {
method: 'POST',

headers: {
'Content-Type': 'application/json',
},

body: JSON.stringify({
cardToken: token,
orderId: 'ORDER-123456',
}),
});

const result = await response.json();

if (!response.ok) {
throw new Error(result.message ?? 'Payment creation failed.');
}

console.log('Pagamento criado:', result);
}

:::note Endpoint do exemplo O endpoint /api/payments pertence ao backend do merchant. Ele não é um endpoint exposto pelo Payment SDK. :::

Requisição para o backend do merchant

POST /api/payments
Content-Type: application/json

{
"cardToken": "tok_abc123",
"orderId": "ORDER-123456"
}

O backend deve utilizar o orderId para recuperar os dados reais da compra.

orderId

├── Cliente da compra
├── Valor real
├── Moeda
├── Produtos
└── Configurações do merchant

Em seguida, o backend utiliza o token para criar o pagamento na API da Orkestral.

POST [ENDPOINT_DE_PAGAMENTO_DA_ORKESTRAL]
Authorization: [CREDENCIAL_PRIVADA]
Content-Type: application/json

{
"token": "tok_abc123",
"amount": 10000,
"currency": "BRL",
"merchantOrderId": "ORDER-123456"
}

:::caution Contrato da API A URL, os headers e o payload anteriores representam somente o fluxo esperado. O contrato real deve seguir a documentação da API de pagamentos da Orkestral. :::

Salvando um cartão

Para salvar o cartão para compras futuras, utilize saveCard: true e informe o customerId.

const result = await card.tokenize({
saveCard: true,
customerId: 'CUSTOMER-123',
customerName: 'João da Silva',
customerDocument: '12345678900',
});
if (result.success) {
console.log('Token temporário:', result.data.token);
console.log('Cartão salvo:', result.data.cardSaved);
console.log('Vault ID:', result.data.vaultId);
}

O token representa os dados tokenizados da operação atual.

O vaultId, quando retornado, representa o identificador reutilizável do cartão salvo para transações futuras.

O merchant não deve armazenar o número completo do cartão, a validade ou o CVV.

Tratamento de erros

interface SDKError {
code: string;
message: string;
field?: 'cardNumber' | 'expiry' | 'cvv';
}

function showTokenizationError(error: SDKError): void {
switch (error.code) {
case 'FIELDS_NOT_READY':
showMessage('Aguarde o carregamento dos campos de cartão.');
break;

case 'MISSING_CUSTOMER_ID':
showMessage('O identificador do cliente é obrigatório para salvar o cartão.');
break;

case 'TOKENIZE_BUSY':
showMessage('Já existe um pagamento sendo processado.');
break;

case 'TOKENIZE_TIMEOUT':
showMessage('A tokenização demorou mais que o esperado. Tente novamente.');
break;

case 'SDK_DESTROYED':
showMessage('O formulário de pagamento não está mais disponível.');
break;

case 'IFRAME_NOT_FOUND':
showMessage('Não foi possível localizar os campos de pagamento.');
break;

default:
showMessage(error.message || 'Não foi possível tokenizar o cartão.');
}
}

function showMessage(message: string): void {
if (paymentMessage) {
paymentMessage.textContent = message;
}
}

Erros controlados pelo SDK

CódigoDescrição
FIELDS_NOT_READYNem todos os Hosted Fields estão prontos
IFRAME_NOT_FOUNDO iframe utilizado na tokenização não foi encontrado
MISSING_CUSTOMER_IDcustomerId não foi informado ao salvar o cartão
SDK_DESTROYEDA instância do cartão já foi destruída
TOKENIZE_BUSYJá existe uma tokenização em andamento
TOKENIZE_TIMEOUTA tokenização ultrapassou o tempo configurado

Outros códigos podem ser retornados pelo ambiente de Hosted Fields ou pelo serviço de tokenização.

Exemplo completo

import { PaymentSDK } from '@orkestralpay/payment-sdk';

const sdk = PaymentSDK.init({
publicKey: 'SUA_CHAVE_PUBLICA',
hostedFieldsUrl: 'URL_DOS_HOSTED_FIELDS',

fieldPaths: {
cardNumber: '/card-number',
expiry: '/expiry',
cvv: '/cvv',
},
});

const card = sdk.createCreditCard({
cardNumber: {
selector: '#card-number',
placeholder: '0000 0000 0000 0000',
},

expiry: {
selector: '#card-expiry',
placeholder: 'MM/AA',
},

cvv: {
selector: '#card-cvv',
placeholder: '123',
},

styles: {
fontSize: '16px',
fontFamily: 'Inter, Arial, sans-serif',
color: '#111827',
padding: '12px',
backgroundColor: 'transparent',
},
});

const completedFields = {
cardNumber: false,
expiry: false,
cvv: false,
};

const readyFields = new Set<string>();

const form = document.querySelector<HTMLFormElement>('#payment-form');
const payButton = document.querySelector<HTMLButtonElement>('#pay-button');
const paymentMessage = document.querySelector<HTMLElement>('#payment-message');

card.on('ready', ({ field }) => {
readyFields.add(field);
updatePayButton();
});

card.on('focus', ({ field }) => {
getFieldContainer(field)?.classList.add('is-focused');
});

card.on('blur', ({ field }) => {
getFieldContainer(field)?.classList.remove('is-focused');
});

card.on('change', ({ field, complete }) => {
completedFields[field] = complete;
updatePayButton();
});

card.on('validation', ({ field, valid, error }) => {
const container = getFieldContainer(field);
const errorElement = getErrorElement(field);

container?.classList.toggle('is-invalid', !valid);

if (errorElement) {
errorElement.textContent = valid ? '' : error ?? 'Campo inválido.';
}
});

card.on('error', ({ field, code, message }) => {
console.error('Payment field error:', {
field,
code,
message,
});
});

form?.addEventListener('submit', async (event) => {
event.preventDefault();

if (!payButton) {
return;
}

payButton.disabled = true;
payButton.textContent = 'Processando...';

try {
const result = await card.tokenize({
saveCard: false,
customerName: 'João da Silva',
customerDocument: '12345678900',

billingAddress: {
street: 'Avenida Paulista, 1000',
city: 'São Paulo',
state: 'SP',
postalCode: '01310100',
country: 'BR',
},
});

if (!result.success) {
showMessage(result.error.message);
return;
}

const response = await fetch('/api/payments', {
method: 'POST',

headers: {
'Content-Type': 'application/json',
},

body: JSON.stringify({
cardToken: result.data.token,
orderId: 'ORDER-123456',
}),
});

const paymentResult = await response.json();

if (!response.ok) {
throw new Error(
paymentResult.message ?? 'Não foi possível criar o pagamento.',
);
}

showMessage('Pagamento realizado com sucesso.');
} catch (error) {
console.error(error);

showMessage(
error instanceof Error
? error.message
: 'Ocorreu um erro inesperado durante o pagamento.',
);
} finally {
payButton.textContent = 'Finalizar pagamento';
updatePayButton();
}
});

function updatePayButton(): void {
if (!payButton) {
return;
}

const fieldsAreReady = readyFields.size === 3;
const fieldsAreComplete = Object.values(completedFields).every(Boolean);

payButton.disabled = !fieldsAreReady || !fieldsAreComplete;
}

function getFieldContainer(field: string): HTMLElement | null {
const selectors: Record<string, string> = {
cardNumber: '#card-number',
expiry: '#card-expiry',
cvv: '#card-cvv',
};

return document.querySelector(selectors[field]);
}

function getErrorElement(field: string): HTMLElement | null {
const selectors: Record<string, string> = {
cardNumber: '#card-number-error',
expiry: '#card-expiry-error',
cvv: '#card-cvv-error',
};

return document.querySelector(selectors[field]);
}

function showMessage(message: string): void {
if (paymentMessage) {
paymentMessage.textContent = message;
}
}

window.addEventListener('beforeunload', () => {
sdk.destroy();
});

Destruição dos campos

Quando o formulário de pagamento não for mais utilizado, remova os recursos criados pelo SDK.

card.destroy();

Para destruir toda a instância:

sdk.destroy();

A destruição da instância remove os iframes, listeners de mensagens, eventos e referências internas.

Segurança

A integração deve seguir os seguintes requisitos:

  1. Nunca criar inputs próprios para número do cartão, validade ou CVV;
  2. Nunca tentar acessar o conteúdo interno dos iframes;
  3. Nunca enviar dados brutos do cartão ao backend do merchant;
  4. Nunca colocar credenciais privadas no frontend;
  5. Utilizar HTTPS em todos os ambientes;
  6. Validar valor, moeda, pedido e cliente no backend;
  7. Não registrar tokens ou dados sensíveis desnecessariamente em logs;
  8. Impedir múltiplos envios durante a tokenização;
  9. Aplicar uma política adequada de Content Security Policy;
  10. Destruir a instância quando o checkout for desmontado.

Checklist de integração

Frontend

  • Instalar o pacote;
  • Receber a chave pública;
  • Configurar a URL dos Hosted Fields;
  • Criar os três containers;
  • Inicializar o SDK;
  • Criar os Hosted Fields;
  • Aguardar os eventos ready;
  • Controlar os eventos de validação;
  • Chamar card.tokenize();
  • Enviar somente o token ao backend;
  • Tratar falhas e timeout;
  • Destruir o SDK ao desmontar o checkout.

Backend

  • Criar um endpoint interno de pagamento;
  • Receber o token e o identificador do pedido;
  • Recuperar o valor real do pedido;
  • Validar cliente, moeda e status do pedido;
  • Autenticar-se na API da Orkestral;
  • Criar o pagamento;
  • Implementar idempotência;
  • Tratar pagamentos aprovados, negados e pendentes;
  • Armazenar somente dados não sensíveis;
  • Implementar logs sem exposição de credenciais.

Informações pendentes

O repositório do Payment SDK não define atualmente:

  • URL oficial dos Hosted Fields;
  • Caminhos oficiais de cada iframe;
  • Processo de geração da chave pública;
  • URL da API de criação do pagamento;
  • Formato de autenticação do backend;
  • Payload oficial da criação do pagamento;
  • Validade e regras de uso do token;
  • Política de idempotência;
  • Status possíveis de uma transação;
  • Fluxo de autenticação 3DS;
  • Webhooks de atualização de pagamento;
  • Fluxo oficial de pagamentos utilizando um vaultId.

Essas informações devem ser adicionadas à documentação quando o contrato da API de pagamentos da Orkestral estiver disponível.