Pular para o conteúdo principal

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
  1. Montar a requisição com o método da operação. O retorno traz a intent e o clientTransactionId, um UUID novo a cada chamada.
  2. Persistir o clientTransactionId antes 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.
  3. Disparar esperando resultado, com ActivityResultLauncher ou startActivityForResult. Com startActivity o host não identifica quem chamou e recusa a operação, mas o seu app não recebe resposta nenhuma.
  4. 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).
  5. Interpretar com parseResponse. A função nunca lança exceção: resposta ausente ou ilegível vira status = ERROR com error.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:

ElementoValor
Pacote (setPackage)com.tupifintech.pos.service
Actioncom.tupifintech.pos.action. + PAYMENT, REFUND, PRINT, QUERY ou ADMIN, conforme a família da operação (tabela abaixo)
Extra com.tupifintech.pos.extra.PROTOCOL_VERSION1 (inteiro)
Extra com.tupifintech.pos.extra.REQUESTJSON da requisição (texto)
Extra de retorno com.tupifintech.pos.extra.RESPONSEJSON da resposta (texto)
  • A action precisa corresponder ao type do payload. Uma venda enviada pela action REFUND, por exemplo, responde MALFORMED_REQUEST.
  • A versão que vale é o campo protocolVersion do JSON; o host não lê o extra PROTOCOL_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 em PosOperation.action, e PosRequest.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).

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çãotypeFamíliaMétodo do clienteSituação
Venda com cartão (crédito, débito, voucher)salePAYMENTsale(SalePayload)Disponível
Venda por Pixpix_salePAYMENTpixSale(PixSalePayload)Disponível
Estorno com cartãorefundREFUNDrefund(RefundPayload)Disponível
Impressão livreprintPRINTprint(PrintPayload)Disponível
Reimpressão de comprovantereprintPRINTreprint(ReprintPayload)Disponível, só últimas 24 h
Listar transaçõeslist_transactionsQUERYlistTransactions(ListTransactionsPayload)Disponível
Detalhe de transaçãoget_transactionQUERYgetTransaction(GetTransactionPayload)Disponível
Situação do terminalterminal_statusADMINterminalStatus()Disponível
Recuperar desfechorecover_resultADMINrecoverResult(id)Disponível
Resolver pendênciasresolve_pendingADMINresolvePending()Disponível
Pedir credenciamentorequest_authorizationADMINrequestAuthorization(motivo)Disponível
Ativar o terminalactivate_terminalADMINactivateTerminal()Disponível, interativa
Listar usuários locaislist_usersADMINlistUsers()Disponível
Cadastrar usuário localcreate_userADMINcreateUser(CreateUserPayload)Disponível, interativa
Alterar usuário localupdate_userADMINupdateUser(UpdateUserPayload)Disponível, interativa
Remover usuário localdelete_userADMINdeleteUser(userId)Disponível, interativa
Pré-autorizaçãopre_authPAYMENTpreAuth(PreAuthPayload)Ainda não
Captura de pré-autorizaçãopre_auth_capturePAYMENTpreAuthCapture(PreAuthCapturePayload)Ainda não
Estorno de Pixpix_refundREFUNDpixRefund(PixRefundPayload)Ainda não

Campos comuns às operações financeiras​

CampoTipoRegra
amountInCentsinteiroValor em centavos. O protocolo não usa ponto flutuante para dinheiro em nenhum campo
operatorTaxIdtexto, opcionalCPF ou CNPJ de quem operou, quando o seu app controla operadores
metadatamapa texto → texto, opcionalDados 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
cardReadTimeoutSecondsinteiro, opcionalQuanto 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​

CampoTipoRegra
amountInCentsinteiro, obrigatórioValor total
paymentMethodcredit, debit, voucher ou unknownunknown (padrão) deixa o portador escolher no terminal
installmentsinteiro1 para à vista (padrão)
installmentsWithInterestbooleanotrue: com juros, financiado pelo emissor. false (padrão): sem juros, financiado pelo lojista
operatorTaxId, metadata, cardReadTimeoutSecondsVer 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​

CampoTipoRegra
transactionIdtexto, opcionalPreenchido: 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
amountInCentsinteiro, opcionalnull estorna o saldo total disponível
paymentMethodopcionalMé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, cardReadTimeoutSecondsVer 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.acquirer informa 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)
BlocoCampos
Texttext, align (left, center, right), bold, textSizePx (padrão 24)
Columnstexts, aligns, bold, textSizePx
QrCodepayload, align (padrão center), sizePx (padrão 200)
ImagecontentUri de um FileProvider seu, widthPx, heightPx, align
Feedlines (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). O PosIntegrationClient.print já concede FLAG_GRANT_READ_URI_PERMISSION.
  • Falhas da impressora respondem PRINTER_ERROR ou NO_PAPER.
  • Problemas no documento (sem blocos, aligns com tamanho diferente de texts, imagem ilegível ou fora de content://) respondem INTERNAL_ERROR, com o motivo em error.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 com terminal_status.
  • Para o comprovante da própria transação, prefira receipt.merchantReceipt e receipt.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_transactionsRegra
fromDateIso, toDateIsoLimites em ISO-8601. null não aplica filtro de data, e o período fica a cargo do backend
paymentMethodFiltro opcional
limitTamanho da página, padrão 50
cursorPaginaçã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_status não movimenta hardware nem rede financeira. Responde em terminal: initialized, terminalId, merchantName, merchantDocument, serialNumber, hostVersionName, supportedProtocolVersions, hasNetworkConnection e pendingTransactions. Use-o para checar o terminal antes de abrir o caixa.
  • resolve_pending dispara a resolução das confirmações e desfazimentos acumulados no terminal. A resposta confirma o agendamento, não a liquidação.
  • recover_result recebe targetClientTransactionId, o id da operação a recuperar, diferente do id desta consulta. Ver Cancelamento e recuperação.
  • request_authorization pede o credenciamento do seu app. Ver Preparação do terminal.
  • activate_terminal abre o cadastro do estabelecimento na tela do terminal, sem campos na requisição. Responde APPROVED com terminal quando o terminal fica ativado (ou já estava) e CANCELLED quando 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çãoCamposO que o terminal pede na tela
list_usersnenhumNada
create_username, cpf, profile (supervisor ou operator)Senha de um Supervisor, se já houver algum, e a senha nova duas vezes
update_useruserId, name e profile opcionais (null mantém o atual), resetPasswordSenha de um Supervisor; com resetPassword = true, também a senha nova
delete_useruserIdSenha de um Supervisor
  • A resposta traz PosUser (id, name, cpf, profile), nunca senha. list_users responde em users; create_user e update_user, em user.
  • 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, responde CANCELLED.
  • 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 responde SUPERVISOR_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_SUPERVISOR em update_user e delete_user, e nome inválido em update_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.