Operações
Toda operação segue os mesmos cinco passos, e o PosIntegrationClient faz a parte mecânica: gera o identificador, monta o JSON e a Intent explícita.
Ciclo de uma operação
1. Montar a requisição PosIntegrationClient.sale(SalePayload(...))
↓
2. Persistir o id request.clientTransactionId, ANTES do launch
↓
3. Disparar com resultado pos.launch(request.intent)
↓
4. Aguardar o terminal conduz cartão, senha e autorização
↓
5. Interpretar PosIntegrationClient.parseResponse(resultCode, data)
e decidir pelo status
- Montar a requisição com o método da operação. O retorno traz a
intente oclientTransactionId, um UUID novo a cada chamada. - Persistir o
clientTransactionIdantes de disparar. Uma venda com cartão pode passar de três minutos, e um Pix fica em consulta enquanto o QR Code for válido. Se o Android encerrar o seu app nesse intervalo, esse id é a única forma de descobrir o desfecho. - Disparar esperando resultado, com
ActivityResultLauncheroustartActivityForResult. ComstartActivityo host não identifica quem chamou e recusa a operação, mas o seu app não recebe resposta nenhuma. - Aguardar. O terminal conduz a operação e o seu app não recebe eventos intermediários. O terminal executa uma operação por vez: não dispare outra enquanto a anterior não devolveu resultado. O host tem um teto por operação: 5 minutos para venda, estorno e Pix, e 60 segundos para impressão. Estourado o teto, responde
OPERATION_TIMEOUT(ver Cancelamento e recuperação). - Interpretar com
parseResponse. A função nunca lança exceção: resposta ausente ou ilegível virastatus = ERRORcomerror.code = NO_RESPONSE. Ver Respostas e erros.
Decida pelo status, nunca pelo resultCode. O host devolve RESULT_OK sempre que produz uma resposta estruturada, inclusive em recusa e terminal ocupado. RESULT_CANCELED significa apenas que o host encerrou sem responder.
Formato na Intent
Se você não usar o PosIntegrationClient, por exemplo num app que não é Kotlin, monte a Intent assim:
| Elemento | Valor |
|---|---|
Pacote (setPackage) | com.tupifintech.pos.service |
| Action | com.tupifintech.pos.action. + PAYMENT, REFUND, PRINT, QUERY ou ADMIN, conforme a família da operação (tabela abaixo) |
Extra com.tupifintech.pos.extra.PROTOCOL_VERSION | 1 (inteiro) |
Extra com.tupifintech.pos.extra.REQUEST | JSON da requisição (texto) |
Extra de retorno com.tupifintech.pos.extra.RESPONSE | JSON da resposta (texto) |
- A action precisa corresponder ao
typedo payload. Uma venda enviada pela actionREFUND, por exemplo, respondeMALFORMED_REQUEST. - A versão que vale é o campo
protocolVersiondo JSON; o host não lê o extraPROTOCOL_VERSION. clientTransactionIdé obrigatório e não pode ser vazio. Sem ele,MALFORMED_REQUEST.- Em Kotlin, as constantes estão em
PosIntent, a action de cada operação emPosOperation.action, ePosRequest.toJson()/PosResponse.fromJson()fazem a serialização.
Requisição de uma venda de R$ 15,00 no crédito à vista. O campo type do payload identifica a operação:
{
"clientTransactionId": "7b1e4c2a-9f0d-4a7e-8d51-2c3b6f9e0a14",
"protocolVersion": 1,
"payload": {
"type": "sale",
"amountInCents": 1500,
"paymentMethod": "credit",
"installments": 1,
"installmentsWithInterest": false,
"metadata": { "pedido": "12345" }
}
}
Resposta aprovada (campos do comprovante resumidos):
{
"clientTransactionId": "7b1e4c2a-9f0d-4a7e-8d51-2c3b6f9e0a14",
"status": "approved",
"protocolVersion": 1,
"receipt": {
"transactionId": "…",
"amountInCents": 1500,
"paymentMethod": "credit",
"authorizationCode": "…",
"nsu": "…",
"cardBrand": "…",
"cardLastDigits": "…",
"acquirer": "…"
}
}
Valores monetários são sempre inteiros em centavos. Enums trafegam em minúsculas (credit, approved, terminal_busy).
Catálogo
A versão 1 do protocolo executa 16 das 19 operações definidas no contrato. As demais já têm formato fixado e respondem UNSUPPORTED_OPERATION até o terminal passar a executá-las.
| Operação | type | Família | Método do cliente | Situação |
|---|---|---|---|---|
| Venda com cartão (crédito, débito, voucher) | sale | PAYMENT | sale(SalePayload) | Disponível |
| Venda por Pix | pix_sale | PAYMENT | pixSale(PixSalePayload) | Disponível |
| Estorno com cartão | refund | REFUND | refund(RefundPayload) | Disponível |
| Impressão livre | print | PRINT | print(PrintPayload) | Disponível |
| Reimpressão de comprovante | reprint | PRINT | reprint(ReprintPayload) | Disponível, só últimas 24 h |
| Listar transações | list_transactions | QUERY | listTransactions(ListTransactionsPayload) | Disponível |
| Detalhe de transação | get_transaction | QUERY | getTransaction(GetTransactionPayload) | Disponível |
| Situação do terminal | terminal_status | ADMIN | terminalStatus() | Disponível |
| Recuperar desfecho | recover_result | ADMIN | recoverResult(id) | Disponível |
| Resolver pendências | resolve_pending | ADMIN | resolvePending() | Disponível |
| Pedir credenciamento | request_authorization | ADMIN | requestAuthorization(motivo) | Disponível |
| Ativar o terminal | activate_terminal | ADMIN | activateTerminal() | Disponível, interativa |
| Listar usuários locais | list_users | ADMIN | listUsers() | Disponível |
| Cadastrar usuário local | create_user | ADMIN | createUser(CreateUserPayload) | Disponível, interativa |
| Alterar usuário local | update_user | ADMIN | updateUser(UpdateUserPayload) | Disponível, interativa |
| Remover usuário local | delete_user | ADMIN | deleteUser(userId) | Disponível, interativa |
| Pré-autorização | pre_auth | PAYMENT | preAuth(PreAuthPayload) | Ainda não |
| Captura de pré-autorização | pre_auth_capture | PAYMENT | preAuthCapture(PreAuthCapturePayload) | Ainda não |
| Estorno de Pix | pix_refund | REFUND | pixRefund(PixRefundPayload) | Ainda não |
Campos comuns às operações financeiras
| Campo | Tipo | Regra |
|---|---|---|
amountInCents | inteiro | Valor em centavos. O protocolo não usa ponto flutuante para dinheiro em nenhum campo |
operatorTaxId | texto, opcional | CPF ou CNPJ de quem operou, quando o seu app controla operadores |
metadata | mapa texto → texto, opcional | Dados do seu app que seguem com a transação até o gateway (pedido, mesa, cliente). Até 20 chaves; chave com até 40 caracteres em [A-Za-z0-9_.-]; valor com até 256; 4 KB no total. Fora disso, MALFORMED_REQUEST |
cardReadTimeoutSeconds | inteiro, opcional | Quanto o terminal espera o cartão em cada tentativa. null usa o padrão (30 s). Aceita de 1 a 300; fora disso, MALFORMED_REQUEST. O número de tentativas e o teto total continuam definidos pelo terminal. Vale só para aquela operação |
Nunca coloque dado de portador em metadata (número do cartão, trilha, CVV, nome impresso). O campo fica gravado no terminal enquanto a transação estiver pendente e segue para o backend. O terminal recusa com MALFORMED_REQUEST valores com cara de número de cartão (13 a 19 dígitos), mas essa checagem é rede de proteção, não autorização.
Venda com cartão: sale
| Campo | Tipo | Regra |
|---|---|---|
amountInCents | inteiro, obrigatório | Valor total |
paymentMethod | credit, debit, voucher ou unknown | unknown (padrão) deixa o portador escolher no terminal |
installments | inteiro | 1 para à vista (padrão) |
installmentsWithInterest | booleano | true: com juros, financiado pelo emissor. false (padrão): sem juros, financiado pelo lojista |
operatorTaxId, metadata, cardReadTimeoutSeconds | Ver campos comuns |
O terminal escolhe o adquirente pela bandeira e pelo produto do cartão. O seu app não escolhe e não precisa saber de antemão: o nome de quem processou volta em receipt.acquirer (null significa não informado).
Voucher (vale-refeição e vale-alimentação) usa o mesmo sale com paymentMethod = VOUCHER e é sempre à vista: installments > 1 é ignorado. O saldo vem em receipt.voucherBalanceInCents quando a adquirente o informa; null significa não informado, e não saldo zerado.
Valor abaixo do mínimo ou acima do máximo do terminal, parcelas acima do permitido, parcela abaixo do valor mínimo e valor acima do limite de contactless respondem MALFORMED_REQUEST, com o motivo em error.message.
Venda por Pix: pix_sale
Campos: amountInCents, operatorTaxId e metadata. O terminal exibe o QR Code e acompanha o pagamento enquanto ele for válido (validade definida pelo backend; 2 minutos quando não informada), mais alguns segundos de folga. Não há leitura de cartão, e a tela do QR Code não tem botão Cancelar.
Estorno com cartão: refund
| Campo | Tipo | Regra |
|---|---|---|
transactionId | texto, opcional | Preenchido: o host escolhe a transação e o operador só apresenta o cartão. Ausente: o host lista as transações estornáveis daquele cartão e o operador escolhe na tela |
amountInCents | inteiro, opcional | null estorna o saldo total disponível |
paymentMethod | opcional | Método da venda original. Informe sempre que souber: em cartão multi-aplicação (crédito e voucher no mesmo plástico), a leitura pode selecionar a aplicação errada sem essa dica |
operatorTaxId, metadata, cardReadTimeoutSeconds | Ver campos comuns |
- Se o cartão apresentado não contiver a transação pedida, a operação é cancelada: o host não estorna "a mais parecida".
- Valor acima do saldo responde
REFUND_EXCEEDS_BALANCE. - O estorno volta pelo adquirente da venda original, e
receipt.acquirerinforma esse adquirente.
Impressão livre: print
O documento é declarativo: você descreve o cupom inteiro e o host o imprime numa passada.
val doc = PrintDocument(listOf(
PrintBlock.Text("MINHA LOJA", align = PrintAlign.CENTER, bold = true),
PrintBlock.Columns(listOf("Café", "R$ 8,00")),
PrintBlock.QrCode("00020126..."),
PrintBlock.Image(contentUri = uri.toString(), widthPx = 384, heightPx = 120),
PrintBlock.Feed(3),
))
pos.launch(PosIntegrationClient.print(PrintPayload(doc)).intent)
| Bloco | Campos |
|---|---|
Text | text, align (left, center, right), bold, textSizePx (padrão 24) |
Columns | texts, aligns, bold, textSizePx |
QrCode | payload, align (padrão center), sizePx (padrão 200) |
Image | contentUri de um FileProvider seu, widthPx, heightPx, align |
Feed | lines (padrão 1) |
- Imagens vão por
content://, porque um bitmap não atravessa a Intent com segurança (limite de cerca de 1 MB do Binder). OPosIntegrationClient.printjá concedeFLAG_GRANT_READ_URI_PERMISSION. - Falhas da impressora respondem
PRINTER_ERRORouNO_PAPER. - Problemas no documento (sem blocos,
alignscom tamanho diferente detexts, imagem ilegível ou fora decontent://) respondemINTERNAL_ERROR, com o motivo emerror.message. - Tipo de bloco ou alinhamento que o host não conhece responde
MALFORMED_REQUEST. Antes de usar um bloco novo, confira a versão do host comterminal_status. - Para o comprovante da própria transação, prefira
receipt.merchantReceiptereceipt.customerReceipt, que já vêm formatados.
Reimpressão: reprint
Campos: transactionId e customerCopy (true imprime a via do cliente, false a do lojista).
Reimprime o cupom gerado pelo gateway, por isso só alcança transações que o seu app fez neste terminal nas últimas 24 horas. Transações de outro app, do app do terminal ou mais antigas respondem TRANSACTION_NOT_FOUND, e a via deve ser obtida no portal.
Consultas: list_transactions e get_transaction
Campo de list_transactions | Regra |
|---|---|
fromDateIso, toDateIso | Limites em ISO-8601. null não aplica filtro de data, e o período fica a cargo do backend |
paymentMethod | Filtro opcional |
limit | Tamanho da página, padrão 50 |
cursor | Paginação: envie o nextCursor da resposta anterior |
A resposta vem em transactions, uma lista com transactionId, amountInCents, status, paymentMethod, createdAtIso, nsu, authorizationCode, cardBrand, cardLastDigits, refundedAmountInCents e acquirer. get_transaction recebe transactionId e responde no mesmo formato, com um item.
O status de cada item é o texto que o backend informa (por exemplo, o de uma transação estornada), e não um PosStatus. Use-o para exibição e conciliação, não para decidir uma venda em curso.
Administração
terminal_statusnão movimenta hardware nem rede financeira. Responde emterminal:initialized,terminalId,merchantName,merchantDocument,serialNumber,hostVersionName,supportedProtocolVersions,hasNetworkConnectionependingTransactions. Use-o para checar o terminal antes de abrir o caixa.resolve_pendingdispara a resolução das confirmações e desfazimentos acumulados no terminal. A resposta confirma o agendamento, não a liquidação.recover_resultrecebetargetClientTransactionId, o id da operação a recuperar, diferente do id desta consulta. Ver Cancelamento e recuperação.request_authorizationpede o credenciamento do seu app. Ver Preparação do terminal.activate_terminalabre o cadastro do estabelecimento na tela do terminal, sem campos na requisição. RespondeAPPROVEDcomterminalquando o terminal fica ativado (ou já estava) eCANCELLEDquando quem está no aparelho desiste. Num terminal já ativado não abre formulário. Ver Preparação do terminal.
Usuários locais
Supervisores e Operadores do terminal podem ser consultados e gerenciados pelo protocolo. Nenhuma senha trafega na Intent: a senha do usuário e a do Supervisor que autoriza são digitadas na tela do terminal.
| Operação | Campos | O que o terminal pede na tela |
|---|---|---|
list_users | nenhum | Nada |
create_user | name, cpf, profile (supervisor ou operator) | Senha de um Supervisor, se já houver algum, e a senha nova duas vezes |
update_user | userId, name e profile opcionais (null mantém o atual), resetPassword | Senha de um Supervisor; com resetPassword = true, também a senha nova |
delete_user | userId | Senha de um Supervisor |
- A resposta traz
PosUser(id,name,cpf,profile), nunca senha.list_usersresponde emusers;create_usereupdate_user, emuser. - São três tentativas de senha de Supervisor. Errar as três ou cancelar responde
AUTHORIZATION_DENIED. Cancelar a senha nova, ou não confirmá-la em três tentativas, respondeCANCELLED. - O primeiro Supervisor é cadastrado sem senha de Supervisor, porque ainda não há quem autorize. Sem Supervisor, cadastrar um Operador responde
SUPERVISOR_REQUIRED. - O terminal mantém de 1 a 10 Supervisores: remover ou rebaixar o último responde
LAST_SUPERVISOR; passar de 10 respondeSUPERVISOR_LIMIT. - Recusas que não dependem de senha (CPF repetido, usuário inexistente, perfil inválido) saem antes de o terminal pedir qualquer senha. A exceção é
LAST_SUPERVISORemupdate_useredelete_user, e nome inválido emupdate_user: esses são conferidos depois da senha do Supervisor. - Falha no armazenamento de usuários do terminal responde
INTERNAL_ERROR. - As operações interativas esperam alguém no aparelho. Trate-as como uma venda e não imponha timeout curto no seu app.