Referência completa do endpoint com exemplos TypeScript. Todos os exemplos usam o handypay ajudante da Início Rápido. O Tooling pode baixar o Contrato OpenAPI 3.1.
URL Base
https://api.handypay.me/api/v1Versão do API: 2025-01-01 (retorno em X-API-Version cabeçalho em cada resposta).
Autenticação
Todos os pedidos requerem um token do portador na Authorization Cabeçalho.
Authorization: Bearer hp_live_your_api_key_here- Chaves prefixadas com
hp_live_são para produção. - Chaves prefixadas com
hp_test_são para o sandbox/testing. - Gerar e gerenciar chaves a partir do Portal do lojista.
O modo de teste está totalmente isolado
Cada hp_test_ A requisição utiliza uma conta de teste dedicada. Produtos de teste, clientes, pagamentos, assinaturas e terminais webhook nunca aparecem em atividades de negócios ao vivo. Veja-os no Espaço de trabalho do modo de teste.
curl https://api.handypay.me/api/v1/test-payments \
-H "Authorization: Bearer hp_test_your_api_key_here"Limites de Taxa
1.000 pedidos por hora por chave do API. Excedendo o limite retorna 429 com uma Retry-After Cabeçalho.
Formato de resposta
Cada resposta é envolto num envelope padrão.
Sucesso
{
"success": true,
"data": { ... },
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}Erro
{
"success": false,
"error": {
"code": "validation_error",
"message": "Name is required"
},
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}Paginação
Todos os parâmetros de lista usam paginação baseada em cursores.
| Campo | Tipo | Requerido | Designação das mercadorias |
|---|---|---|---|
| limit | number | Não | Itens por página (1–100, padrão 10) |
| starting_after | string | Não | ID do último item da página anterior |
Resposta inclui has_more: true quando existem páginas adicionais.
Produtos
Crie e gerencie produtos para compras únicas.
| Método | Localização | Designação das mercadorias |
|---|---|---|
| POST | /v1/products | Criar um produto |
| GET | /v1/products | Produtos da lista |
| GET | /v1/products/:id | Obter um produto |
| PUT | /v1/products/:id | Actualizar um produto |
| DELETE | /v1/products/:id | Arquivar um produto |
Criar um produto
const product = await handypay("/products", {
method: "POST",
body: JSON.stringify({
name: "Premium Plan",
description: "Access to all features",
price: {
amount: 2999,
currency: "usd",
},
}),
});
console.log(product.id); // "prod_abc123"exemplo cURL
curl -X POST https://api.handypay.me/api/v1/products \
-H "Authorization: Bearer hp_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Premium Plan",
"description": "Access to all features",
"price": {
"amount": 2999,
"currency": "usd"
}
}'Organismo de pedido
| Campo | Tipo | Requerido | Designação das mercadorias |
|---|---|---|---|
| name | string | Sim. | Denominação do produto |
| description | string | Não | Descrição do produto |
| images | string[] | Não | Até 8 URLs de imagens |
| metadata | object | Não | Pares de valor de chave personalizados (até 50 teclas) |
| active | boolean | Não | Verdadeiro padrão |
| url | string | Não | Página do produto URL em seu site |
| shippable | boolean | Não | Se o produto requer ou não envio |
| unit_label | string | Não | Rótulo por unidade (por exemplo: "sede", "licença") |
| statement_descriptor | string | Não | Texto do extrato bancário (máximo de 22 caracteres) |
| tax_code | string | Não | Código fiscal do Stripe |
| price.amount | number | Não | Preço na unidade monetária mais pequena (cents) |
| price.currency | string | Não | Código monetário do ISO 4217 (por exemplo: "usd", "jmd") |
| price.tax_behavior | string | Não | Inclusivo, exclusivo ou não especificado |
Clientes
Gerencie registros de clientes para compras e assinaturas repetidas.
| Método | Localização | Designação das mercadorias |
|---|---|---|
| POST | /v1/customers | Criar um cliente |
| GET | /v1/customers | Listar clientes |
| GET | /v1/customers/:id | Obter um cliente |
| PUT | /v1/customers/:id | Atualizar um cliente |
| DELETE | /v1/customers/:id | Apagar um cliente |
Criar um cliente
const customer = await handypay("/customers", {
method: "POST",
body: JSON.stringify({
email: "customer@example.com",
name: "Jane Doe",
}),
});
console.log(customer.id); // "cus_abc123"exemplo cURL
curl -X POST https://api.handypay.me/api/v1/customers \
-H "Authorization: Bearer hp_live_..." \
-H "Content-Type: application/json" \
-d '{
"email": "customer@example.com",
"name": "Jane Doe"
}'Organismo de pedido
| Campo | Tipo | Requerido | Designação das mercadorias |
|---|---|---|---|
| string | Sim. | E- mail do cliente | |
| name | string | Não | Nome do cliente |
| phone | string | Não | Número de telefone do cliente |
| metadata | object | Não | Pares de valor de chave personalizados |
Sessões de pagamento
Crie sessões de checkout hospedadas para pagamentos únicos. Os preços padrão do HandyPay se aplicam: 4,9% + US$0.40 por transação no plano gratuito, ou 4,2% + US$0.40 no Pro. Não há taxa extra no API ou plataforma no topo.
| Método | Localização | Designação das mercadorias |
|---|---|---|
| POST | /v1/payment-sessions | Criar uma sessão de pagamento |
| GET | /v1/payment-sessions/:id | Obter o estado da sessão |
| GET | /v1/test-payments | Pagamentos de teste da lista (apenas para hp_test_) |
Criar uma sessão de pagamento (com o preço existente)
const session = await handypay("/payment-sessions", {
method: "POST",
body: JSON.stringify({
line_items: [{ price_id: "price_abc123", quantity: 1 }],
success_url: "https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url: "https://yoursite.com/cancel",
}),
});
// Redirect customer to checkout
window.location.href = session.url;exemplo cURL
curl -X POST https://api.handypay.me/api/v1/payment-sessions \
-H "Authorization: Bearer hp_live_..." \
-H "Content-Type: application/json" \
-d '{
"line_items": [{ "price_id": "price_abc123", "quantity": 1 }],
"success_url": "https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}",
"cancel_url": "https://yoursite.com/cancel"
}'Criar uma sessão de pagamento (quantidade personalizada)
const session = await handypay("/payment-sessions", {
method: "POST",
body: JSON.stringify({
line_items: [{
amount: 5000,
currency: "usd",
name: "Custom Order",
quantity: 1,
}],
success_url: "https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url: "https://yoursite.com/cancel",
}),
});exemplo cURL
curl -X POST https://api.handypay.me/api/v1/payment-sessions \
-H "Authorization: Bearer hp_live_..." \
-H "Content-Type: application/json" \
-d '{
"line_items": [{
"amount": 5000,
"currency": "usd",
"name": "Custom Order",
"quantity": 1
}],
"success_url": "https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}",
"cancel_url": "https://yoursite.com/cancel"
}'Organismo de pedido
| Campo | Tipo | Requerido | Designação das mercadorias |
|---|---|---|---|
| line_items | array | Sim. | Pelo menos uma linha de item |
| success_url | string | Sim. | Redirecionar o URL após o pagamento bem-sucedido |
| cancel_url | string | Sim. | Redirecionar o URL se o cliente cancelar |
| customer_id | string | Não | Cliente existente ID |
| customer_email | string | Não | Pré-preencher o e-mail (se não houver customer_id) |
| pass_fees_to_customer | boolean | Não | Adicionar taxas de processamento e serviço ao total do cliente |
| metadata | object | Não | Pares de valor de chave personalizados |
| collect_shipping_address | boolean | Não | Recolha um endereço de envio |
| billing_address_collection | string | Não | auto ou necessário |
| shipping_countries | string[] | Não | Países de destino permitidos do ISO-2 |
| shipping_options | array | Não | Selos e montantes de envio na unidade monetária mais pequena |
Campos de itens de linha
| Campo | Tipo | Requerido | Designação das mercadorias |
|---|---|---|---|
| price_id | string | Não | Stripe Price ID existente |
| amount | number | Não | Quantidade personalizada em cêntimos |
| currency | string | Não | Obrigatório com montante |
| name | string | Não | Obrigatório com montante |
| quantity | number | Sim. | Quantidade |
Fornecer qualquer um dos price_id ou amount+currency+name por item de linha.
{CHECKOUT_SESSION_ID} no URL de sucesso. Consulte que ID com o mesmo comerciante e o mesmo modo de chave live/test que o criou. A sessão continua questionável após a conclusão, mas seu webhook assinado ainda deve conduzir o cumprimento da fatura.Pagamentos Incorporados
Crie um PaymentIntent no seu servidor quando quiser renderizar o Stripe Elements na sua própria página de checkout. A chave da API HandyPay permanece no servidor; envie ao navegador apenas a chave publicável retornada, o ID da conta conectada e o segredo de cliente de curta duração.
| Método | Localização | Designação das mercadorias |
|---|---|---|
| POST | /v1/payment-intents | Criar um PaymentIntent integrado |
const intent = await handypay("/payment-intents", {
method: "POST",
body: JSON.stringify({
amount: 5000,
currency: "ttd",
description: "Order #1042",
customer_email: "buyer@example.com",
pass_fees_to_customer: true,
metadata: { order_id: "1042" },
}),
});
// Pass these values to Stripe.js/Elements. Never pass HANDYPAY_API_KEY.
return {
clientSecret: intent.client_secret,
publishableKey: intent.publishable_key,
stripeAccount: intent.stripe_account,
};| Campo | Tipo | Requerido | Designação das mercadorias |
|---|---|---|---|
| amount | number | Sim. | Número inteiro positivo na unidade monetária mais pequena |
| currency | string | Sim. | ISO 4217 código de moeda de três letras |
| description | string | Não | Designação do pagamento |
| customer_email | string | Não | Email do cliente para recibos e reconciliação |
| pass_fees_to_customer | boolean | Não | Reembolso do valor para que o cliente cubra as taxas |
| metadata | object | Não | Identificadores de sua encomenda ou fatura |
Montante das restituições
Reembolso de um pagamento de propriedade do comerciante autenticado. O HandyPay verifica a propriedade do pagamento e o saldo disponível antes de criar a inversão. Omitir amount para um reembolso total.
| Método | Localização | Designação das mercadorias |
|---|---|---|
| POST | /v1/refunds | Criar um reembolso total ou parcial |
const refund = await handypay("/refunds", {
method: "POST",
body: JSON.stringify({
session_id: "cs_live_...",
amount: 2500,
reason: "requested_by_customer",
}),
});
console.log(refund.id, refund.status);| Campo | Tipo | Requerido | Designação das mercadorias |
|---|---|---|---|
| session_id | string | Condicional | ID da Checkout Session (cs_...). Use este campo ou payment_intent |
| payment_intent | string | Condicional | ID do PaymentIntent (pi_...). Use este campo ou session_id |
| amount | number | Não | Montante parcial da restituição na unidade monetária mais pequena |
| reason | string | Não | duplicate, fraudulent ou requested_by_customer |
Um pagamento contestado, totalmente reembolsado, de troca cruzada ou insuficiente é rejeitado sem criar um reembolso.
Litígios
Reveja os encargos para o comerciante conectado e fornecer evidências antes da data de vencimento.
| Método | Localização | Designação das mercadorias |
|---|---|---|
| GET | /v1/disputes | Listar litígios |
| GET | /v1/disputes/:id | Obter uma disputa e seu status de evidência |
| POST | /v1/disputes/:id/evidence | Actualizar ou apresentar elementos de prova |
const dispute = await handypay("/disputes/dp_123/evidence", {
method: "POST",
body: JSON.stringify({
evidence: {
customer_email_address: "buyer@example.com",
product_description: "Annual software subscription",
customer_communication: "https://files.example.com/evidence/1042.pdf",
},
submit: false,
}),
});| Campo | Tipo | Requerido | Designação das mercadorias |
|---|---|---|---|
| evidence | object | Não | Stripe disputa campos de evidência como valores de string |
| submit | boolean | Não | Definir o valor verdadeiro apenas quando a evidência estiver completa e pronta para revisão |
Assinaturas
Crie produtos recorrentes e gerencie assinaturas.
Produtos de assinatura
| Método | Localização | Designação das mercadorias |
|---|---|---|
| POST | /v1/subscription-products | Criar um produto de subscrição |
| GET | /v1/subscription-products | Listar produtos de assinatura |
Sessões de Assinatura & Gestão
| Método | Localização | Designação das mercadorias |
|---|---|---|
| POST | /v1/subscription-sessions | Criar a verificação da subscrição |
| GET | /v1/subscriptions | Listar as assinaturas activas |
| PATCH | /v1/subscriptions/:id/quantity | Mudar de assento com pronunciação explícita |
| POST | /v1/subscriptions/:id/cancel | Cancelar no final do período de faturamento |
Intervalos de faturamento suportados
Criar um produto de subscrição
const subProduct = await handypay("/subscription-products", {
method: "POST",
body: JSON.stringify({
name: "Pro Plan",
description: "Monthly pro access",
amount: 1999,
currency: "usd",
interval: "monthly",
trial_period_days: 14,
}),
});
console.log(subProduct.price.id); // Use this price_id for subscription sessionsexemplo cURL
curl -X POST https://api.handypay.me/api/v1/subscription-products \
-H "Authorization: Bearer hp_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Pro Plan",
"description": "Monthly pro access",
"amount": 1999,
"currency": "usd",
"interval": "monthly",
"trial_period_days": 14
}'Organismo de pedido
| Campo | Tipo | Requerido | Designação das mercadorias |
|---|---|---|---|
| name | string | Sim. | Denominação do produto |
| description | string | Não | Descrição do produto |
| amount | number | Sim. | Preço na unidade monetária mais pequena |
| currency | string | Sim. | ISO 4217 código monetário |
| interval | string | Sim. | Um dos intervalos suportados |
| trial_period_days | number | Não | Duração da experiência gratuita em dias |
| metadata | object | Não | Pares de valor de chave personalizados |
Criar checkout com vários assentos
const session = await handypay("/subscription-sessions", {
method: "POST",
body: JSON.stringify({
price_id: subProduct.price.id,
quantity: 5,
customer_email: "buyer@example.com",
success_url: "https://example.com/success",
cancel_url: "https://example.com/plans",
}),
});Mudar de lugar numa subscrição activa
Quantidade deve ser um inteiro de 1 a 1.000. Escolha como o ajuste de faturamento é tratado em vez de confiar em um padrão implícito.
const updated = await handypay("/subscriptions/sub_123/quantity", {
method: "PATCH",
body: JSON.stringify({
quantity: 8,
proration_behavior: "create_prorations",
}),
});| Campo | Tipo | Requerido | Designação das mercadorias |
|---|---|---|---|
| quantity | number | Sim. | Nova contagem de assentos de 1 a 1.000 |
| proration_behavior | string | Não | create_prorations, always_invoice ou none |
| item_id | string | Não | Rubrica específica da subscrição quando uma subscrição tem vários produtos |
Webhooks
Receba notificações de eventos em tempo real através do HTTP POST para seus objetivos.
| Método | Localização | Designação das mercadorias |
|---|---|---|
| POST | /v1/webhook-endpoints | Registar um ponto final |
| GET | /v1/webhook-endpoints | Endpoints da lista |
| DELETE | /v1/webhook-endpoints/:id | Desactivar um ponto final |
- Os pontos de extremidade devem usar o HTTPS.
- Endpoints Webhook registrados com um
hp_test_A chave recebe apenas os eventos de teste. Os destinos vivos e de ensaio são armazenados separadamente. - Cada endpoint ativo subscrito a um evento recebe uma entrega independente assinada com o segredo próprio desse endpoint. Não reutilize o segredo de um ponto final para outro.
- Retorne uma resposta de 2xx em 10 segundos. Armazenar cada evento
idantes de efeitos secundários, por isso, as entregas duplicadas são seguras. - Após 10 falhas consecutivas de entrega, um ponto final é automaticamente desativado.
Tipos de eventos suportados
- payment_intent.succeeded
- payment_intent.payment_failed
- checkout.session.completed
- checkout.session.expired
- checkout.session.async_payment_succeeded
- checkout.session.async_payment_failed
- customer.subscription.created
- customer.subscription.updated
- customer.subscription.deleted
- charge.refunded
- charge.dispute.created
- charge.dispute.closed
payment_intent.payment_failed o evento identifica o PaymentIntent em data.id; use seus metadados (por exemplo order_id) para conciliá-lo. Utilização checkout.session.expired para a expiração da sessão e os eventos assinc para os métodos de pagamento atrasados.Verificando assinaturas do webhook
Cada entrega inclui uma X-HandyPay-Signature cabeçalho no formato sha256={hex}. Verifique computando o HMAC-SHA256 do corpo de solicitação bruto usando o segredo de assinatura do seu endpoint:
import { createHmac, timingSafeEqual } from "node:crypto";
function verifySignature(
payload: string | Buffer,
secret: string,
signature: string
): boolean {
const prefix = "sha256=";
if (!signature.startsWith(prefix)) return false;
const hex = signature.slice(prefix.length);
if (!/^[0-9a-f]{64}$/i.test(hex)) return false;
const expected = createHmac("sha256", secret).update(payload).digest();
const received = Buffer.from(hex, "hex");
return received.length === expected.length && timingSafeEqual(received, expected);
}Manipulador de rota de API do Next.js
import { NextRequest, NextResponse } from "next/server";
import { createHmac, timingSafeEqual } from "node:crypto";
export const runtime = "nodejs";
const SECRET = process.env.HANDYPAY_WEBHOOK_SECRET;
if (!SECRET) throw new Error("HANDYPAY_WEBHOOK_SECRET is not configured");
function hasValidSignature(payload: string, signature: string): boolean {
const prefix = "sha256=";
if (!signature.startsWith(prefix)) return false;
const hex = signature.slice(prefix.length);
if (!/^[0-9a-f]{64}$/i.test(hex)) return false;
const expected = createHmac("sha256", SECRET).update(payload).digest();
const received = Buffer.from(hex, "hex");
return received.length === expected.length && timingSafeEqual(received, expected);
}
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = req.headers.get("x-handypay-signature") ?? "";
if (!hasValidSignature(body, signature)) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
let event: { id: string; type: string; data: unknown };
try {
event = JSON.parse(body);
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
// Persist event.id before side effects so redeliveries can be ignored safely.
switch (event.type) {
case "checkout.session.completed":
// Fulfill an immediate payment.
break;
case "checkout.session.async_payment_succeeded":
// Fulfill a delayed payment.
break;
case "charge.refunded":
// Mark the matching order as refunded.
break;
}
return NextResponse.json({ received: true });
}Manipulador Express.js
// Register this route BEFORE app.use(express.json()).
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const router = express.Router();
const SECRET = process.env.HANDYPAY_WEBHOOK_SECRET;
if (!SECRET) throw new Error("HANDYPAY_WEBHOOK_SECRET is not configured");
function hasValidSignature(payload: Buffer, signature: string): boolean {
const prefix = "sha256=";
if (!signature.startsWith(prefix)) return false;
const hex = signature.slice(prefix.length);
if (!/^[0-9a-f]{64}$/i.test(hex)) return false;
const expected = createHmac("sha256", SECRET).update(payload).digest();
const received = Buffer.from(hex, "hex");
return received.length === expected.length && timingSafeEqual(received, expected);
}
router.post(
"/handypay",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = String(req.headers["x-handypay-signature"] ?? "");
if (!hasValidSignature(req.body, signature)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(req.body.toString("utf8"));
// Persist event.id before side effects so redeliveries are idempotent.
console.log("Verified HandyPay event", event.id, event.type);
return res.json({ received: true });
}
);
export default router;Formato de carga útil Webhook
{
"id": "evt_abc123",
"type": "payment_intent.succeeded",
"created": 1706745600,
"data": { ... }
}Conta
Leia a carga e disponibilidade de pagamento do comerciante conectado, além da conta bancária de pagamento padrão. Os números de banco e roteamento são mascarados; apenas seus últimos quatro dígitos são devolvidos.
| Método | Localização | Designação das mercadorias |
|---|---|---|
| GET | /v1/account | Obter conta conectada e detalhes de pagamento mascarados |
const account = await handypay("/account");
console.log({
chargesEnabled: account.chargesEnabled,
payoutsEnabled: account.payoutsEnabled,
bank: account.bankAccount?.bankName,
last4: account.bankAccount?.last4,
});Códigos de Erros
| Código | HTTP | Designação das mercadorias |
|---|---|---|
| unauthorized | 401 | Chave do API em falta ou inválida |
| key_revoked | 401 | Chave API foi revogada |
| key_expired | 401 | A chave do API expirou |
| rate_limit_exceeded | 429 | Muitos pedidos |
| validation_error | 400 | A validação do corpo da solicitação falhou |
| invalid_url | 400 | success_url ou cancel_url inválido |
| invalid_interval | 400 | Intervalo de faturamento não suportado |
| product_not_found | 404 | O produto não existe |
| customer_not_found | 404 | O cliente não existe |
| session_not_found | 404 | A sessão de saída não existe |
| payment_not_found | 404 | O pagamento está faltando ou não é propriedade deste comerciante |
| subscription_not_found | 404 | A assinatura não existe |
| refund_not_allowed | 400 | O pagamento é contestado e não pode ser reembolsado |
| already_refunded | 400 | O pagamento já está totalmente reembolsado |
| refund_amount_too_large | 400 | Montante superior ao saldo remanescente reembolsável |
| insufficient_balance | 400 | O saldo disponível não pode cobrir a restituição |
| balance_verification_failed | 400 | Não foi possível verificar a propriedade ou disponibilidade de saldos |
| endpoint_not_found | 404 | Endpoint do API desconhecido |
| stripe_error | 502 | Stripe API retornou um erro |
| internal_error | 500 | Erro inesperado no servidor |
| payload_too_large | 413 | O organismo de solicitação excede 1MB |
| prohibited_content | 400 | O conteúdo viola a política de uso aceitável |
| webhook_url_must_be_https | 400 | Webhook URL deve usar HTTPS |
| key_creation_rate_exceeded | 429 | Muitas chaves criadas na janela de tempo |
| max_keys_reached | 400 | Teclas API máximas ativas alcançadas (25) |
Segurança
- Manter
hp_live_ehp_test_chaves no seu servidor. Nunca coloque-os no navegador JavaScript, aplicativos móveis, URLs, logs ou controle de fonte. - Todas as respostas incluem
X-Content-Type-Options: nosniff,X-Frame-Options: DENY, eStrict-Transport-SecurityCabeçalhos. - Solicitar limite do corpo: 1 MiB, incluindo pedidos em fluxo ou em blocos.
- Os terminais Webhook devem usar o HTTPS.
- Verifique assinaturas do webhook contra os bytes brutos exatos e compare digestos em tempo constante.
- Os nomes e descrições dos produtos são analisados contra uma lista de bloqueio de conteúdo proibida.
- Mais 5 violações de conteúdo em 24 horas suspenderá suas chaves do API.
Limites de chaves de API
- Max 3 chaves criadas por janela de 10 minutos.
- Max 10 chaves criadas por janela de 1 hora.
- Max 25 chaves activas por comerciante.