Paysell

Aceite pagamentos em cripto

A Paysell liquida TON e USDT na rede TON. Você cria uma fatura, nós entregamos um link, e você recebe um callback assinado assim que o dinheiro é confirmado na blockchain e creditado no seu saldo.

Visão geral#

O que a Paysell faz, e o que não faz.

A Paysell é um processador de pagamentos, não uma carteira. Você nunca lida com chaves privadas, monitora a blockchain ou decide quando uma transação é definitiva — isso é conosco.

Cada fatura recebe seu próprio endereço de recebimento. Quando um comprador a paga, esperamos a rede confirmar a transferência, descontamos nossa taxa e creditamos o restante no seu saldo. Você saca para qualquer endereço que quiser.

Os saldos ficam conosco e são a única fonte de verdade. Exiba-os, mas nunca mantenha uma segunda cópia como autoritativa — dois contadores sempre acabam divergindo, e então ninguém sabe qual está certo.

Como funciona um pagamento#

Seis passos, a maioria nossos.

Seis passos, a maioria nossos:

  1. 1

    Seu cliente clica em pagar

    Seu servidor chama nossa API com o valor e sua própria referência de pedido.

  2. 2

    Entregamos um endereço

    Um endereço de recebimento novo é retirado de um pool pré-gerado e vinculado a esta fatura. Um endereço pertence exatamente a uma fatura aberta, e é assim que um pagamento é associado a ela.

  3. 3

    O cliente envia as moedas

    Ele escaneia o QR code ou copia o endereço. Envie-o para a payment_url que retornamos e a página já é resolvida por nós — valor, endereço, QR, contagem regressiva, status ao vivo.

  4. 4

    Detectamos a transferência

    Duas fontes independentes de dados da blockchain são consultadas, e suas respostas são comparadas. Se divergirem, paramos em vez de escolher a resposta mais conveniente.

  5. 5

    Esperamos a finalidade

    Inclusão na masterchain mais três blocos acima. Cerca de quinze segundos — um pagamento que parece confirmado e depois some seria sua perda, então não corremos esse risco.

  6. 6

    Creditado, e você é avisado

    A taxa é descontada, o restante cai no seu saldo, e um webhook assinado vai para o seu servidor com seu order_id.

Do pagamento ao callback: cerca de um minuto — uns quinze segundos de confirmações de rede, o resto é nossa varredura de endereços monitorados.

Para onde vai o dinheiro#

A taxa, e sobre o que ela é calculada.

A taxa é de 0,2%, fixa para sua loja no momento em que é registrada. Se a taxa padrão mudar depois, a sua não muda — ela é gravada em cada fatura como um número, não como referência a uma configuração.

A taxa é cobrada sobre o que realmente chega, não sobre o que a fatura pedia. Fature 5 USDT e receba 20, a taxa é calculada sobre 20. Pagou a menos? É calculada sobre o que chegou.

example
Invoice:   5.000000 USDT
Received: 20.000000 USDT   (the buyer sent more)
Fee 0.2%:  0.040000 USDT   (on 20, not on 5)
Credited: 19.960000 USDT

O pagamento a mais é creditado integralmente — não ficamos com a diferença. O pagamento a menos deixa a fatura aberta para o comprador completar no mesmo endereço.

Tirar moedas de um endereço de recebimento custa gás de rede, e nós pagamos — essa parte nunca encosta no seu saldo. Sacar para o seu próprio endereço é outra coisa: tem taxa própria, descontada do valor que você pedir, e os números exatos estão na Tabela de taxas.

Início rápido#

Cinco minutos até sua primeira fatura.

Cinco passos. Dois são cliques na sua área do cliente, um é uma única requisição do seu servidor, e os dois últimos acontecem sozinhos.

  1. 1

    Crie uma loja

    Na sua área do cliente. Ela começa a aceitar pagamentos imediatamente — sem esperar revisão. A verificação acontece silenciosamente em segundo plano e só restringe saques, não pagamentos recebidos.

  2. 2

    Emita uma chave de API

    Sua loja → Chaves de API → Nova chave. A chave e o segredo do webhook são mostrados uma única vez e nunca mais. Guarde-os como guardaria a senha de um banco de dados, e nunca os envie para o navegador.

  3. 3

    Crie uma fatura

    Uma requisição do seu servidor, um link de volta. Os quatro trechos abaixo enviam exatamente a mesma coisa.

  4. 4

    Mande o comprador para a payment_url

    Isso é o checkout inteiro — valor, endereço, QR code, contagem regressiva, status ao vivo — e não há nada para você construir. Veja Checkout para o que o comprador realmente vê.

  5. 5

    Espere o webhook

    Assim que o dinheiro é confirmado on-chain e creditado, fazemos um POST de um evento payment.credited assinado para o seu servidor. Verifique a assinatura e então marque o pedido como pago — mas só quando data.status for paid ou overpaid. Veja Webhooks.

The same request, four ways

curl -X POST https://paysell.me/api/merchant/v1/invoices \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "asset": "USDT_TON",
    "amount": "5",
    "order_id": "order-1042",
    "idempotency_key": "order-1042"
  }'

Redirecione o comprador para a payment_url da resposta. Pronto — o resto chega como webhook.

What to do next

Autenticação#

Sua chave de API, e como ela é usada.

Toda requisição carrega sua chave no cabeçalho Authorization:

http
Authorization: Bearer sk_live_IH4SNdnYsv-yabTuPCVln4vOvvGoEyxA

Toda chave emitida aqui começa com sk_live_. O prefixo sk_test_ só existe em uma implantação apontada para a rede de testes, e nenhuma implantação dessas é oferecida — veja Testes. Guardamos um hash irreversível, não a chave em si, então ninguém, nem nós, pode mostrá-la de novo. Perdeu? Emita uma nova e revogue a antiga.

A loja é derivada da chave, por isso nenhuma requisição leva um id de loja. Uma chave só pode agir sobre sua própria loja.

O caminho leva versão: /api/merchant/v1/…. Dentro de uma versão só acrescentamos campos — nada é renomeado nem muda de sentido em silêncio. Uma mudança que quebraria o seu código ganha um novo prefixo, /v2, e /v1 continua funcionando por um prazo anunciado.

Esta chave cria faturas em seu nome. Mantenha-a no servidor. Qualquer coisa em JavaScript de navegador é pública, por mais bem escondida que pareça.

Endpoints at a glance#

Four calls, three of them authenticated.

This is the whole merchant API. Balances, payouts and history are not in it — they live in your account area, where a person is looking at them.

EndpointMethodAuthWhat it does
/invoicesPOSTAPI keyOpen an invoice and get a payment link. Details.
/invoices/{invoice_id}GETAPI keyRead one invoice's current state. Details.
/invoices/{invoice_id}/cancelPOSTAPI keyClose an invoice that is still open and free its address. Details.
/public/invoices/{invoice_id}GETnoneWhat the hosted checkout page reads. Only needed if you build your own. Details.

Every path is relative to https://paysell.me/api/merchant/v1. There is no list endpoint and no refund endpoint — see Refunds.

Criar uma fatura#

POST /api/merchant/v1/invoices

POST/api/merchant/v1/invoices

Corpo da requisição

CampoTipoObrigatórioDescrição
assetstringsimTON ou USDT_TON.
amountstringsimUnidades normais da moeda, como string: "5" são 5 USDT. Não mais casas decimais do que a moeda tem. Ver Valores.
order_idstringnãoSua própria referência, até 200 caracteres. Volta em todo webhook — é assim que você liga um pagamento a um pedido.
descriptionstringnãoAté 1000 caracteres. Mostrado ao comprador na página de pagamento.
ttl_minutesnumbernãoPor quanto tempo a fatura continua pagável, em minutos. 1–1440; omita e vale o padrão — hoje 2 horas.
idempotency_keystringnãoAté 200 caracteres. Envie o mesmo valor ao repetir e você recebe a mesma fatura de volta em vez de uma segunda. É um campo do corpo, não o cabeçalho Idempotency-Key — esse cabeçalho não é lido aqui.

Resposta · 201

json
{
  "invoice_id": "12c22c1a-a496-4c1e-abe3-72661ef8706e",
  "payment_url": "https://paysell.me/pay/12c22c1a-a496-4c1e-abe3-72661ef8706e",
  "address": "UQAvDJp7QDwqRcuNQBiK2GhBt71Xh1_UMYPCzMkQAoBPmZKl",
  "asset": "USDT_TON",
  "amount": "5",
  "amount_minor": "5000000",
  "status": "pending",
  "paid": "0",
  "paid_minor": "0",
  "order_id": "order-1042",
  "description": "Pro subscription",
  "expires_at": "2026-09-06T17:20:55Z",
  "created_at": "2026-09-06T15:20:55Z"
}

Como usar isso no seu pedido

CampoO que fazer com ele
invoice_idGuarde-o junto ao seu pedido. É o que identifica o pagamento em tudo mais.
payment_urlRedirecione o comprador para cá. Não há mais nada para construir.
addressSó se você montar seu próprio checkout. Mostre-o exatamente como recebido — veja o aviso abaixo.
amountO valor em unidades normais, exatamente como você enviou. Exiba este.
amount_minorO mesmo valor como inteiro na unidade mínima. Calcule com este.
expires_atMostre uma contagem regressiva. Depois que passar, o endereço para de ser monitorado para esta fatura.
statusAqui é sempre pending. Mudanças reais chegam por webhook.
Se você montar sua própria página, imprima o endereço exatamente como retornado. Ele está em forma não-reembolsável (UQ… na mainnet, 0Q… na testnet). Convertê-lo, embelezá-lo ou trocá-lo por outra codificação do mesmo endereço faz com que moedas enviadas a uma carteira ainda não implantada voltem ao remetente.

Consultar uma fatura#

GET /api/merchant/v1/invoices/{invoice_id}

GET/api/merchant/v1/invoices/{invoice_id}

Mesmo formato acima, com status, paid e paid_minor refletindo o presente: paid é quanto chegou em unidades normais, e paid_minor o mesmo como inteiro na unidade mínima. Útil como reserva quando um webhook foi perdido, ou numa página de agradecimento.

Consulte no máximo a cada poucos segundos, e trate webhooks como o canal principal. Faturas de outra loja respondem 404 — não 403, então um id não pode ser sondado quanto à existência.

Cancelar uma fatura#

POST /api/merchant/v1/invoices/{invoice_id}/cancel

POST/api/merchant/v1/invoices/{invoice_id}/cancel

Fecha uma fatura que ainda está aberta — pending ou underpaid — e libera seu endereço. Use quando o cliente abandona o checkout: endereços são um recurso finito, e devolvê-los mantém o pool saudável.

Uma fatura que não está mais aberta responde 409. Cancelar uma underpaid não devolve moedas a ninguém: o dinheiro já creditado continua no seu saldo, e o que se encerra é apenas a aceitação de um complemento.

Webhooks#

O que chega, e como verificar.

Defina uma URL de webhook ao criar a chave. Fazemos um POST para ela quando um pagamento é creditado — e quando um depósito retido para uma verificação adicional é recusado. Toda entrega é assinada, e continuamos tentando por cerca de um dia e meio até você responder 2xx. Libere a mercadoria com status: paid ou overpaid, não pela simples chegada da chamada.

Eventos

EventoQuandoO que o corpo traz
payment.creditedA transferência foi confirmada on-chain, nossa taxa foi retida e o restante está no seu saldo.Os campos listados abaixo.
payment.rejectedUm depósito retido para uma verificação adicional (veja Referência de status) foi recusado. O dinheiro não vai chegar ao seu saldo.invoice_id, order_id, asset, amount, tx_hash e reason. Não libere a mercadoria; se a fatura já estava paid por uma transferência anterior, este evento é sobre o depósito extra, não sobre aquele pagamento.

O que chega

json
{
  "event_id": "99f74f58-efbb-4af1-b0a3-76b0073f9e6b",
  "type": "payment.credited",
  "data": {
    "invoice_id": "12c22c1a-a496-4c1e-abe3-72661ef8706e",
    "order_id": "order-1042",
    "asset": "USDT_TON",
    "amount": "5000000",
    "credited": "4905000",
    "fee": "95000",
    "status": "paid",
    "paid_minor": "5000000",
    "tx_hash": "97a1f0…"
  }
}
json
{
  "event_id": "0a1b2c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d",
  "type": "payment.rejected",
  "data": {
    "invoice_id": "12c22c1a-a496-4c1e-abe3-72661ef8706e",
    "order_id": "order-1042",
    "asset": "USDT_TON",
    "amount": "5000000",
    "tx_hash": "97a1f0…",
    "reason": "could not be matched to any order"
  }
}

Mapeamento de campos

CampoSignificado
event_idÚnico por evento; também vem no cabeçalho X-Paysell-Event-Id. Guarde-o e ignore repetições — veja abaixo.
data.order_idSua referência. Procure seu pedido por ela.
data.amountO que o comprador enviou nesta transferência, na unidade mínima — ao contrário da API, que recebe unidades normais.
data.feeO que retivemos, na unidade mínima.
data.creditedO que caiu no seu saldo: amount − fee, na unidade mínima.
data.paid_minorTotal recebido nesta fatura até agora, na unidade mínima. O campo que importa em underpaid: o status diz que chegou menos, este diz quanto a menos.
data.assetA moeda que de fato chegou. Não necessariamente a moeda que a fatura pedia.
data.asset_mismatchPresente, e igual a true, apenas quando a moeda que chegou não é a da fatura. O dinheiro é creditado a você, mas a fatura continua não paga e status nunca será paid.
data.invoice_assetVem junto com asset_mismatch: a moeda que a fatura realmente pede.
data.statusO status da fatura agora: pending, underpaid, paid, overpaid ou expired. Compare com o que você esperava.
data.tx_hashA transação on-chain, para seus registros e suporte.

Cabeçalhos em toda entrega

http
X-Paysell-Event:      payment.credited
X-Paysell-Event-Id:   99f74f58-efbb-4af1-b0a3-76b0073f9e6b
X-Paysell-Timestamp:  1789000000
X-Paysell-Signature:  sha256=6f1c0e6a…
CabeçalhoSignificado
X-Paysell-EventO tipo do evento: payment.credited ou payment.rejected.
X-Paysell-Event-IdÚnico por evento. É este o valor pelo qual deduplicar.
X-Paysell-TimestampQuando assinamos, em segundos unix. Faz parte da string assinada.
X-Paysell-Signaturesha256= seguido do HMAC em hexadecimal. Veja abaixo.

Verificando a assinatura

Toda requisição é assinada com o segredo do webhook mostrado uma única vez quando você criou a chave. A assinatura é HMAC-SHA256(secret, "{timestamp}.{raw_body}") — o timestamp de X-Paysell-Timestamp, um ponto literal, e então os bytes do corpo. Verifique antes de agir: sem isso, qualquer um que descubra sua URL pode te entregar um pedido pago.

Python:

python
import hmac, hashlib, time

def is_ours(body: bytes, signature: str, timestamp: str, secret: str) -> bool:
    if abs(time.time() - int(timestamp)) > 300:      # ±5 minutes
        return False
    signed = timestamp.encode() + b"." + body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    # compare_digest, not ==: a plain comparison leaks the answer through timing
    return hmac.compare_digest("sha256=" + expected, signature)

Node.js:

javascript
const crypto = require("node:crypto")

function isOurs(body, signature, timestamp, secret) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false  // ±5 min
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(timestamp + ".").update(body)   // body is the raw Buffer, not a parsed object
    .digest("hex")
  const a = Buffer.from(expected), b = Buffer.from(signature)
  // timingSafeEqual throws when the lengths differ, so check that first
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Assine os bytes brutos do corpo, exatamente como recebidos. Se você analisar o JSON e re-serializá-lo, os bytes mudam — ordem das chaves, espaçamento — e a assinatura não vai bater. Compare em tempo constante (hmac.compare_digest, crypto.timingSafeEqual): um == comum retorna mais rápido quando o primeiro byte está errado, e essa diferença basta para adivinhar uma assinatura byte a byte.

A janela do timestamp

Rejeite tudo cujo timestamp esteja a mais de cinco minutos do seu próprio relógio, em qualquer direção. O timestamp está dentro da string assinada justamente para não poder ser editado sem quebrar a assinatura; a janela é o que transforma isso em proteção. Sem ela, uma requisição capturada uma vez continua válida para sempre e pode ser reenviada a qualquer momento — a assinatura sozinha nunca expira. Mantenha o relógio do seu servidor no NTP, ou esta verificação vai começar a rejeitar entregas legítimas.

Duplicatas

O mesmo evento pode chegar mais de uma vez. Isso não é um bug: repetimos até você responder 2xx, e uma entrega que teve sucesso mas cuja resposta nunca chegou até nós é enviada de novo. Registre X-Paysell-Event-Id (ele também vem como event_id no corpo) e faça a segunda chegada não fazer nada.

Novas tentativas

A primeira tentativa sai assim que o pagamento é creditado. Se ela falhar — timeout, conexão recusada, erro de TLS, um redirecionamento, ou qualquer status fora do 2xx — tentamos de novo em um cronograma fixo:

1 min → 5 min → 15 min → 1 h → 6 h → 24 h

Sete tentativas no total, espalhadas por cerca de 31 horas. As primeiras ficam próximas porque a causa habitual é um receptor que estava reiniciando e já voltou; as últimas ficam esparsas porque martelar um servidor que está fora do ar há um dia não ajuda ninguém.

Depois da última tentativa a entrega é marcada como dropped e paramos por conta própria. Ela não se perde: a linha do pagamento na sua área do cliente mostra o estado, a contagem de tentativas e a classe do erro, com um botão Enviar novamente que inicia uma nova rodada das sete tentativas. Seu outro recurso é GET /api/merchant/v1/invoices/{invoice_id} — a fatura sempre sabe o próprio status.

Como deve ser uma URL de webhook

A URL é verificada quando você a salva, e de novo antes de cada entrega. Uma URL que não passa na verificação recebe 422 e code: "webhook_url_rejected" no momento de salvar, e marca a entrega como failed — sem novas tentativas — se começar a falhar depois. As regras:

  • Apenas `https://`, e porta 443. Um webhook carrega dados de pagamento; em http puro eles são legíveis por qualquer um no caminho.
  • Um nome de domínio, não um endereço IP. Você precisa de um certificado de qualquer forma, e certificados não são emitidos para IPs nus.
  • Nada de `localhost`, nem nomes .local, .internal, .corp, .lan ou .test — nossos servidores não alcançam a sua rede, e um nome que resolve dentro da nossa é exatamente o que não podemos chamar.
  • Sem credenciais na URL (https://user:pass@…). Coloque seu próprio token no caminho ou em um parâmetro de consulta, se precisar de um.
  • Todo endereço para o qual o nome resolve precisa ser público — A e AAAA, os dois. Faixas privadas, de loopback, link-local e CGNAT são recusadas, e a verificação se repete antes de cada entrega, então apontar o registro para 127.0.0.1 depois também não funciona.
  • Um redirecionamento é uma falha, não um salto. Não os seguimos: o endereço que você nos deu foi verificado, o do cabeçalho Location não foi.
A verificação acontece duas vezes de propósito — uma quando você salva a URL, para que um erro de digitação seja respondido na hora em vez de virar entrega silenciosa que nunca acontece, e outra antes de cada envio, porque o dono de um domínio pode reapontá-lo para um endereço interno a qualquer momento. Se o seu endpoint mudar, atualize a chave primeiro: uma URL rejeitada não entrega nada e não fica na fila.

Responda rápido

Qualquer 2xx serve, dentro de dez segundos — esse é todo o nosso timeout, conexão incluída. Responda primeiro, faça o trabalho lento depois; um endpoint que espera pelo próprio banco de dados antes de responder acabará registrado como timeout e será tentado de novo, e você vai processar o mesmo evento duas vezes. Qualquer outra coisa — um 4xx, um 5xx, um redirecionamento, um travamento — conta como tentativa falha e volta para o cronograma acima.

Entrega, com honestidade

O que é garantido é o mecanismo de entrega: sete tentativas ao longo de cerca de 31 horas, um reenvio manual pela sua área do cliente, e um endpoint de fatura que sempre sabe o status real. Monte o fluxo de modo que um webhook que nunca chega não te custe nada — leia a fatura na sua página de agradecimento, ou concilie as faturas abertas a cada hora. Webhooks são o caminho rápido, não o único.

A complete receiver#

Signature, deduplication and a fast answer, end to end.

The snippets above verify one signature. This is the whole endpoint: raw body, signature check, deduplication by event_id, a fast 2xx, and the one condition that is allowed to mark an order paid.

Node.js with Express. express.raw is the part people get wrong: express.json() hands you a parsed object, and bytes you re-serialise from it are not the bytes we signed.

javascript
const express = require("express")
const crypto = require("node:crypto")

const app = express()
const SECRET = process.env.PAYSELL_WEBHOOK_SECRET

function isOurs(body, signature, timestamp) {
  const sentAt = Number(timestamp)
  if (!Number.isFinite(sentAt)) return false
  if (Math.abs(Date.now() / 1000 - sentAt) > 300) return false   // ±5 minutes

  const expected = "sha256=" + crypto
    .createHmac("sha256", SECRET)
    .update(timestamp + ".").update(body)      // raw Buffer, not a parsed object
    .digest("hex")

  const a = Buffer.from(expected), b = Buffer.from(signature ?? "")
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

app.post(
  "/paysell/webhook",
  express.raw({ type: "application/json" }),   // NOT express.json()
  async (req, res) => {
    const signature = req.get("X-Paysell-Signature")
    const timestamp = req.get("X-Paysell-Timestamp")
    if (!isOurs(req.body, signature, timestamp)) return res.sendStatus(401)

    const event = JSON.parse(req.body.toString("utf8"))

    // Answer first: 10 seconds is the whole timeout, connection included.
    res.sendStatus(200)

    // Deduplicate. In real code this is a unique column, not a Set.
    if (await alreadyHandled(event.event_id)) return
    await remember(event.event_id)

    if (event.type !== "payment.credited") return
    const { order_id, status, credited, asset, tx_hash } = event.data

    // The only condition that may release the goods.
    if (status !== "paid" && status !== "overpaid") return
    await markOrderPaid(order_id, { credited, asset, tx_hash })
  }
)

Python with Flask. request.get_data() is the raw body; request.form and request.json are not.

python
import hashlib
import hmac
import json
import os
import time

from flask import Flask, request

app = Flask(__name__)
SECRET = os.environ["PAYSELL_WEBHOOK_SECRET"]


def is_ours(body: bytes, signature: str, timestamp: str) -> bool:
    try:
        sent_at = int(timestamp)
    except (TypeError, ValueError):
        return False
    if abs(time.time() - sent_at) > 300:              # ±5 minutes
        return False

    signed = timestamp.encode() + b"." + body
    expected = hmac.new(SECRET.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest("sha256=" + expected, signature)


@app.post("/paysell/webhook")
def paysell_webhook():
    body = request.get_data()                          # raw bytes, unparsed
    if not is_ours(body, request.headers.get("X-Paysell-Signature", ""),
                   request.headers.get("X-Paysell-Timestamp", "")):
        return "", 401

    event = json.loads(body)

    # Deduplicate. In real code this is a unique column, not a set.
    if already_handled(event["event_id"]):
        return "", 200                                 # a repeat is still a success
    remember(event["event_id"])

    if event["type"] == "payment.credited":
        data = event["data"]
        # The only condition that may release the goods.
        if data["status"] in ("paid", "overpaid"):
            mark_order_paid(data["order_id"], data)

    return "", 200                                     # 2xx within 10 seconds

What the code is doing, and why

  • Verify before anything acts on the body. An unsigned request that reaches your business logic is a paid order for whoever found your URL.
  • Answer 2xx first, work afterwards. Ten seconds is the whole timeout, connection included. A handler that waits for its own database gets recorded as a timeout and retried, and you process the same event twice.
  • Deduplicate on `event_id` in storage that survives a restart. The in-memory set in the examples keeps them short; a real one is a unique column in your database.
  • Mark the order paid only on `status: paid` or `overpaid`. underpaid means part of the money arrived and the invoice is still open, and a deposit in the wrong coin never makes an invoice paid either.
  • Answer 2xx to a duplicate too. A repeat that gets a 4xx looks like a failure to us and comes back again on the schedule.

payment.rejected arrives at the same endpoint. It means a deposit held for an additional check was declined and the money will not be credited: release nothing, and if the invoice was already paid by an earlier transfer, this event is about the extra deposit, not about that payment.

Referência de status#

Todos os status de fatura e pagamento, explicados.

Fatura

StatusSignificadoO que fazer
pendingAguardando pagamento.Mantenha o pedido aberto.
paidPaga integralmente.Libere o produto.
overpaidChegou mais do que o pedido. O excedente é creditado a você integralmente.Libere o produto; reembolse a diferença se quiser.
underpaidChegou menos do que o pedido. A fatura continua aberta e mantém seu endereço: o comprador pode complementar no mesmo lugar, e paid_minor diz quanto já entrou. Ela segue pagável pelo resto da sua vida útil mais um período de tolerância de 24 horas após expires_at.Espere o complemento, ou acerte com o cliente. Não libere o produto — a fatura não está paga.
expiredA janela fechou, período de tolerância incluído. Ainda pode carregar dinheiro: o que chegou permaneceu no seu saldo, e paid_minor diz quanto.Ofereça uma nova fatura. Não aceite pagamento no endereço antigo: assim que uma fatura expira, o endereço volta para o pool, e uma transferência muito atrasada vira caso de suporte em vez de crédito automático. Confira paid_minor antes de dizer ao cliente que nada foi recebido.
cancelledCancelada por você. O endereço volta para o pool.Nada.

Pagamento

Visível na sua área do cliente; útil ao dar suporte a um cliente durante o pagamento.

StatusSignificado
detectedVisto on-chain, aguardando confirmações.
confirmedA rede confirmou. Creditando em seguida.
creditedNo seu saldo. É quando o webhook dispara.
reviewRetido para uma verificação adicional — por exemplo, moedas chegando a um endereço sem fatura aberta.
rejectedNão creditado. O motivo é registrado.

Quando um pagamento vai para `review`

Alguns depósitos ficam retidos para uma verificação adicional em vez de serem creditados na hora: um valor incomumente alto, moedas chegando a um endereço sem fatura aberta, ou as duas fontes de blockchain que consultamos discordando sobre o que aconteceu. Nada se perde — o dinheiro espera por uma decisão e o webhook dispara assim que ela sai, o que pode levar minutos ou horas. Trate a ausência de callback em um pagamento exibido como review como algo normal, e não como falha. Se isso importa para um pedido, fale com o suporte e informe o tx_hash.

Valores#

Unidades normais ao enviar, unidades mínimas ao receber.

Envie os valores nas unidades normais da moeda, como string"1.5" é um e meio. Não um número JSON, nem a unidade mínima.

AtivoCasas decimaisVocê enviaamount_minor na resposta
TON9"1.5""1500000000"
USDT_TON6"1.5""1500000"

String e não número, porque números JSON são doubles IEEE-754 e um valor grande em nanotons deixa de caber neles com exatidão. Mais casas decimais do que a moeda tem é 422, nunca um arredondamento silencioso do seu dinheiro. Nos webhooks é o contrário: ali amount, fee e credited são inteiros na unidade mínima, porque aquele lado é lido por código, não por uma pessoa.

javascript
// Send amounts in the coin's normal units, as a string:
const amount = "1.5"   // one and a half TON or USDT

// In responses, amount is that same human string; amount_minor is the
// integer in smallest units — use it for exact maths, as a string or BigInt:
BigInt(invoice.amount_minor)  // e.g. 1500000n

Limites#

Mínimos, máximos e limites de taxa.

LimiteValorAo violar
Fatura mínima0.1 TON · 3 USDT422
Fatura máxima7000 TON · 10000 USDT422
Faturas por hora, por loja60429
Faturas abertas ao mesmo tempo20, crescendo a cada fatura paga, até 200429
Vida útil da fatura1 minuto – 24 horas (padrão 2 horas)422
Requisições de API por chave120 por minuto429 + Retry-After

O mínimo não é burocracia. Nossa taxa é percentual, mas receber um pagamento custa um valor fixo: tirar USDT de um endereço de recebimento significa abastecê-lo com gás primeiro, do nosso próprio bolso. Abaixo de alguns dólares a taxa não cobre o manuseio, e aceitar tal pagamento significaria creditar a você dinheiro que não compensa movimentar.

O máximo não existe por causa de grandes lojas — é uma armadilha para o erro de unidades. Envie "5000000" onde queria "5" e, sem ele, você teria uma fatura de cinco milhões de dólares: o comprador vê um valor absurdo e vai embora. Um pedido real nunca encosta nesse teto; um erro sempre encosta. Os dois tetos são configurações (invoice_max_ton, invoice_max_usdt) e podem ser elevados para a sua loja — é só pedir.

O limite por hora e o limite de faturas abertas protegem o pool de endereços. Cada fatura aberta ocupa um endereço de recebimento, e um loop descontrolado em um site esgotaria o pool para todo mundo. Uma loja nova pode manter 20 faturas abertas ao mesmo tempo; a cota cresce em uma unidade a cada fatura que ela efetivamente recebeu, até o teto de 200. underpaid conta como aberta — ela ainda está ocupando seu endereço, esperando o restante. Cancelar uma fatura abandonada devolve o endereço imediatamente. Repetições com a mesma idempotency_key não contam para o limite por hora.

O limite de requisições é 120 por minuto por chave de API — duas chamadas por segundo, bem acima de qualquer fluxo real de pedidos. Um 429 traz um cabeçalho Retry-After em segundos: espere esse tempo em vez de repetir em um laço apertado, o que só empurra a janela para mais longe.

Erros#

Os códigos de status que você realmente vai ver.

Os erros voltam como JSON, em dois formatos. Tudo o que nós ou o núcleo de processamento decidimos coloca um par {code, message} dentro de detail. Um corpo de requisição que não passa na validação coloca ali, em vez disso, uma lista de erros de campo. Verifique qual dos dois você recebeu antes de ler detail.code — e ramifique pelo `code`, nunca pela `message`: o texto pode mudar a qualquer momento, o código não.

json
{
  "detail": {
    "code": "invalid_input",
    "message": "invoice amount below the minimum: 0.010000 USDT_TON, minimum 3.000000 USDT_TON"
  }
}
json
{
  "detail": [
    {
      "type": "string_type",
      "loc": ["body", "amount"],
      "msg": "Input should be a valid string",
      "input": 5
    }
  ]
}
StatusQuandoO que fazer
401Chave ausente, incorreta ou revogada.Verifique o cabeçalho. Reemita a chave se ela foi revogada.
404Não existe tal fatura, ou ela pertence a outra loja.Verifique o id. Os dois casos respondem igual de propósito, para que um id não possa ser sondado.
409A fatura está em um estado que proíbe isso.Leia o status atual primeiro.
422A requisição está malformada, ou o valor está fora dos limites da fatura.A mensagem indica tanto o valor enviado quanto o limite.
429Faturas demais nesta hora, abertas demais ao mesmo tempo, ou requisições demais.Aguarde o Retry-After e tente de novo.
502Não conseguimos alcançar o núcleo de processamento.Tente novamente com a mesma chave de idempotência.

Códigos

O formato decidido por nós é {"detail": {"code": …, "message": …}}. Estes são os códigos que a API do lojista retorna.

CódigoStatusSignificado
invalid_api_key401A chave está ausente, malformada, é desconhecida ou foi revogada. Os quatro casos respondem igual, então uma chave não pode ser sondada.
not_found404Não existe tal objeto, ou ele pertence a outra loja.
invalid_input422A requisição não passou na validação do núcleo — um valor inválido, casas decimais demais, um valor fora dos limites da fatura.
conflict409A ação contradiz o estado atual, como cancelar uma fatura que não está mais aberta.
too_many_requests429Um limite de taxa: faturas por hora, faturas abertas, ou requisições por minuto. Retry-After diz quanto tempo esperar.
cbc_unreachable502Não conseguimos alcançar o núcleo de processamento. Tente novamente com a mesma idempotency_key.
webhook_url_rejected422Só ao salvar uma chave: a URL de webhook não passou nas verificações acima. detail.reason indica qual regra — scheme, port, ip_literal, local_hostname, private_address, dns_error, credentials, e assim por diante.

Um 502 não significa que a fatura não foi criada — a requisição pode ter sido concluída com a resposta perdida no caminho de volta. Tente novamente com a mesma idempotency_key e você recebe a fatura existente ou uma nova, nunca duas.

Checkout: what the buyer sees#

The hosted payment page, and when to build your own.

payment_url points at https://paysell.me/pay/{invoice_id}. One page, no account, no login, mobile first, and nothing for you to build.

On the page

  • Your shop's name, the amount and the coin, large, with the invoice's description underneath.
  • A countdown to expires_at — plus the 24-hour grace period when the invoice is underpaid.
  • The invoice's short id with a copy button, so a buyer can quote it to your support.
  • Wallet buttons: Tonkeeper and MyTonWallet open with the address and the amount already filled in. Other reveals a QR code and the address with a copy button.
  • A warning that only this invoice's coin, on the TON network, may be sent — anything else is lost.

How the page reacts

InvoiceWhat the buyer sees
pending“Waiting for payment”, with the wallet choices and the countdown. The page re-reads the invoice every five seconds.
underpaid“Received X of Y”, the exact remainder still owed, and the same address to send it to. The wallet link is prefilled with what is missing, not with the original total — otherwise the buyer would pay twice.
paid · overpaid“Payment received”, and a button back to your shop if the shop has a URL.
expired“Payment window closed”. If money did arrive, the amount is named with a note to contact you — silence here would send the buyer looking for their coins.
cancelled“Payment cancelled”, with a link back to your shop.

If you build your own

You gain your own branding and take on all of the above: the exact address string, the right coin, the countdown with its grace period, the underpayment case, and polling. GET /api/merchant/v1/public/invoices/{invoice_id} is the same unauthenticated read the hosted page uses — rate limited per IP, so poll it no more often than every few seconds. Print the address exactly as returned.

Typical integration mistakes#

The handful that account for most broken integrations.

None of these are exotic. Every one of them has cost somebody a day.

  • Sending the amount as a number

    {"amount": 5} is a 422. It has to be the string "5": JSON numbers are IEEE-754 doubles, and a large sum in nanotons stops being exactly representable in one.

  • Sending the smallest unit

    "5000000" for 5 USDT is a mistake in units, and the upper invoice limit exists to catch it. Smallest units are what comes back in webhooks, not what goes out in requests.

  • Treating the webhook's arrival as payment

    Read data.status. underpaid is not paid, and a deposit in a coin the invoice did not ask for never makes it paid either. Release the goods on paid or overpaid, on nothing else.

  • Not checking the timestamp

    A signature on its own never expires. Without the ±5 minute window on X-Paysell-Timestamp, a delivery captured once can be replayed at any time and will still verify.

  • Verifying the signature over re-serialised JSON

    Parse the body and serialise it again and the bytes change — key order, spacing — and the HMAC no longer matches. Sign the raw bytes exactly as received.

  • No deduplication

    The same event_id will arrive twice sooner or later: we retry until you answer 2xx, and a response lost on the way back looks like a failure from here. The second arrival must do nothing.

  • Using the Idempotency-Key header

    This API reads idempotency_key from the request body; the header is not read at all. A retry without the field opens a second invoice for the same order.

  • Assuming detail is always an object

    It is {code, message} for anything we or the core decide, and a list of field errors when the body itself fails validation. Check which one you got before reading detail.code.

Reembolsos#

Como reembolsar um cliente.

O reembolso é feito pelo suporte, não por uma chamada de API. Um reembolso é uma nova transferência para um endereço que uma pessoa forneceu, e um processador de pagamentos que devolve dinheiro automaticamente numa chamada de API é um processador que pode ser levado a mandar dinheiro para o endereço de um atacante. Por isso é deliberadamente manual.

Para reembolsar um comprador, abra um chamado de suporte pela sua área do cliente com o invoice_id ou o tx_hash, o valor, e o endereço de destino. Um operador confere o pagamento, tira o dinheiro do seu saldo e responde no mesmo chamado. Conte com um dia útil, não com um minuto.

Duas consequências que valem ser consideradas no seu desenho. O pagamento a mais é creditado a você integralmente — não ficamos com nada dele — então devolver a diferença a um comprador que enviou demais é decisão sua e segue o mesmo caminho. E uma fatura com pagamento a menos não é caso de reembolso enquanto continua aberta: o dinheiro está no seu saldo, o endereço segue monitorado, e o comprador pode simplesmente completar. Só depois do período de tolerância, quando a fatura vai para expired com dinheiro nela, é que existe uma decisão a tomar.

Testes#

Como testar a sua integração antes do lançamento.

As chaves aqui são de produção: toda chave emitida é uma chave sk_live_ contra o núcleo de produção e a mainnet TON. Não existe um ambiente de teste separado, e isso tem um lado bom: você percorre exatamente o caminho que os seus pedidos reais vão percorrer.

Então teste como você testaria qualquer coisa que envolva dinheiro real: com valores pequenos. Crie uma fatura no mínimo (0.1 TON ou 3 USDT), pague-a da sua própria carteira e acompanhe o caminho inteiro — a página de pagamento, o webhook, a verificação da assinatura, seu pedido virando pago. A taxa se aplica, e as moedas realmente se movem.

As partes que você pode exercitar sem gastar nada: criar e ler uma fatura, cancelar uma, o 422 em um valor malformado, o 401 em uma chave errada, e a sua própria verificação de assinatura — assine um corpo de exemplo com o seu segredo e passe-o para o seu próprio handler. O que realmente exige um pagamento real é só o último passo: um webhook payment.credited de verdade.

Planeje a integração de modo que ela não dependa de uma sandbox ou de um pagamento simulado: o caminho real é mais rápido — e mais fiel — de verificar.

Trate seu primeiro pedido real como o teste de verdade: escolha um valor pequeno, mantenha a fatura aberta no painel, e confira a linha do pagamento e o estado do webhook antes de apontar clientes reais para lá.

For AI agents and LLMs#

Machine-readable copies of this page, and a prompt to start from.

Everything on this page also exists in a form a model can read directly. Point your assistant at one of these instead of pasting screenshots of documentation into a chat.

The three files

FileWhat it isUse it for
/llms-full.txtThe whole documentation as one markdown file: endpoints, fields, statuses, limits, errors, webhooks with working verification code, the fee, the checkout page, the checklist.Pasting into a model's context, or letting an agent fetch it. Start here.
/llms.txtA short index in the llms.txt format: what Paysell is, the five rules that decide whether an integration works, and links to everything else.Letting an agent discover the rest on its own.
/openapi.jsonOpenAPI 3.1, generated from the running application's own models, both webhook events included.Generating a client, or loading into anything that speaks OpenAPI.

A prompt to start from

Copy this, replace the stack, and hand it to your assistant. It names the four things that go wrong most often, so the answer does not have to be corrected afterwards.

prompt
Read https://paysell.me/llms-full.txt and implement Paysell payments in my <stack>:
create invoices (POST /api/merchant/v1/invoices, Bearer sk_live_ key, amount as a
decimal string in normal units), redirect the buyer to payment_url, verify webhook
signatures (HMAC-SHA256 over "{timestamp}.{raw_body}", header X-Paysell-Signature,
reject anything whose X-Paysell-Timestamp is more than 300 seconds off), deduplicate
by event_id, answer 2xx within 10 seconds, and mark orders paid only on a
payment.credited event whose data.status is "paid" or "overpaid".

Feeding it to a specific tool

  • Agents with web access — Claude Code, Cursor, Windsurf and the like: give them the /llms-full.txt link. One fetch, no setup.
  • A chat window — ChatGPT, Claude, Gemini: paste the contents of /llms-full.txt into the conversation or attach it as a file. It is written to fit in one message.
  • OpenAPI tooling — client generators, Postman, an agent's tool schema: point it at https://paysell.me/openapi.json. Its servers entry already carries the production base URL, so generated calls go to the right place.

Checklist antes de ir ao ar#

Dez itens para conferir antes do lançamento.

  • A chave fica só no servidor, nunca no JavaScript do navegador.
  • A assinatura do webhook é verificada contra "{timestamp}.{raw_body}", em tempo constante.
  • Entregas com mais de cinco minutos são rejeitadas, e o relógio do servidor está no NTP.
  • Um X-Paysell-Event-Id repetido não faz nada na segunda vez.
  • O webhook responde 2xx em até dez segundos; o trabalho lento acontece depois.
  • A URL de webhook é um domínio https:// na porta 443, sem nenhum redirecionamento na frente.
  • Um webhook perdido é contornável: o endpoint da fatura é lido na página de agradecimento ou em uma varredura de conciliação.
  • idempotency_key é gerada uma vez por pedido e reutilizada nas repetições.
  • Os valores saem em unidades normais como strings; os números do webhook são lidos como unidades mínimas.
  • O endereço é exibido exatamente como retornado, sem modificação.
  • overpaid e underpaid são tratados, não apenas paid; expired ainda pode carregar paid_minor.
  • A mercadoria é liberada com status: paid ou overpaid, nunca pela simples chegada do callback.
  • 429 é tratado esperando o Retry-After, não repetindo na hora.
  • Os saldos são lidos de nós, não controlados à parte como verdade.

Algo não ficou claro?

Se esta página não respondeu à sua pergunta, isso é uma lacuna na documentação e vale a pena nos contar. Escreva pela sua área do cliente e vamos corrigir a página, não só a resposta.