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:
- Instalar e inicializar o SDK;
- Criar os containers dos campos de cartão;
- Acompanhar os eventos de preenchimento e validação;
- Solicitar a tokenização dos dados;
- Enviar o token ao backend do merchant.
Responsabilidades do backend
O backend do merchant deve:
- Receber o token gerado pelo SDK;
- Validar os dados da compra;
- Recuperar o valor real do pedido;
- Autenticar-se na API da Orkestral;
- Criar o pagamento utilizando o token;
- 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âmetro | Obrigatório | Descrição |
|---|---|---|
publicKey | Sim | Chave pública utilizada para identificar o merchant |
hostedFieldsUrl | Sim | URL base do servidor responsável pelos Hosted Fields |
fieldPaths.cardNumber | Sim | Caminho do iframe do número do cartão |
fieldPaths.expiry | Sim | Caminho do iframe da validade |
fieldPaths.cvv | Sim | Caminho do iframe do CVV |
sessionId | Não | Identificador da sessão de pagamento |
tokenizeTimeout | Não | Tempo 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:
| Propriedade | Descrição |
|---|---|
field | Campo alterado |
empty | Indica se o campo está vazio |
complete | Indica 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
| Propriedade | Obrigatório | Descrição |
|---|---|---|
saveCard | Não | Indica se o cartão deve ser salvo |
customerId | Condicional | Obrigatório quando saveCard for true |
customerName | Não | Nome completo do titular |
customerDocument | Não | Documento do titular |
billingAddress.street | Não | Endereço |
billingAddress.city | Não | Cidade |
billingAddress.state | Não | Estado ou região |
billingAddress.postalCode | Não | CEP |
billingAddress.country | Não | Có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ódigo | Descrição |
|---|---|
FIELDS_NOT_READY | Nem todos os Hosted Fields estão prontos |
IFRAME_NOT_FOUND | O iframe utilizado na tokenização não foi encontrado |
MISSING_CUSTOMER_ID | customerId não foi informado ao salvar o cartão |
SDK_DESTROYED | A instância do cartão já foi destruída |
TOKENIZE_BUSY | Já existe uma tokenização em andamento |
TOKENIZE_TIMEOUT | A 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:
- Nunca criar inputs próprios para número do cartão, validade ou CVV;
- Nunca tentar acessar o conteúdo interno dos iframes;
- Nunca enviar dados brutos do cartão ao backend do merchant;
- Nunca colocar credenciais privadas no frontend;
- Utilizar HTTPS em todos os ambientes;
- Validar valor, moeda, pedido e cliente no backend;
- Não registrar tokens ou dados sensíveis desnecessariamente em logs;
- Impedir múltiplos envios durante a tokenização;
- Aplicar uma política adequada de Content Security Policy;
- 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.