Referencia completa de puntos finales con ejemplos de TypeScript. Todos los ejemplos utilizan el handypay ayudante del Inicio rápido. Las herramientas pueden descargar el contrato OpenAPI 3.1.
URL base
https://api.handypay.me/api/v1Versión de API: 2025-01-01 (devuelta en X-API-Version encabezado en cada respuesta).
Autenticación
Todas las solicitudes requieren un token de portador en el Authorization encabezado.
Authorization: Bearer hp_live_your_api_key_here- Las claves con el prefijo
hp_live_son para producción. - Las claves con el prefijo
hp_test_son para sandbox/pruebas. - Generar y administrar claves desde Portal de comercios.
El modo de prueba está completamente aislado
Cada hp_test_ solicitud utiliza una cuenta de prueba dedicada. Los productos de prueba, los clientes, los pagos, las suscripciones y los puntos finales de webhooks nunca aparecen en la actividad comercial en vivo. Véalos en el espacio de trabajo del modo de prueba.
curl https://api.handypay.me/api/v1/test-payments \
-H "Authorization: Bearer hp_test_your_api_key_here"límites de velocidad
1000 solicitudes por hora por clave API. Superar el límite regresa 429 con un Retry-After encabezado.
Formato de respuesta
Cada respuesta está envuelta en un sobre estándar.
Éxito
{
"success": true,
"data": { ... },
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}Error
{
"success": false,
"error": {
"code": "validation_error",
"message": "Name is required"
},
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}Paginación
Todos los puntos finales de la lista utilizan paginación basada en cursor.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| limit | number | No | Artículos por página (1–100, por defecto 10) |
| starting_after | string | No | ID del último artículo de la página anterior |
La respuesta incluye has_more: true cuando hay páginas adicionales existen.
Productos
Crear y administrar productos para compras únicas.
| Método | Ruta | Descripción |
|---|---|---|
| POST | /v1/products | Crear un producto |
| GET | /v1/products | Lista de productos |
| GET | /v1/products/:id | Consiga un producto |
| PUT | /v1/products/:id | Actualizar un producto |
| DELETE | /v1/products/:id | Archivo de un producto |
Crear un producto
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"ejemplo de 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"
}
}'Solicitud body
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| name | string | Sí. | Nombre del producto |
| description | string | No | Descripción del producto |
| images | string[] | No | Hasta 8 URLs de imagen |
| metadata | object | No | Parejas de valor clave personalizadas (hasta 50 teclas) |
| active | boolean | No | Default true |
| url | string | No | Página de producto URL en su sitio |
| shippable | boolean | No | Si el producto requiere envío |
| unit_label | string | No | Etiqueta por unidad (por ejemplo. "sello", "license" |
| statement_descriptor | string | No | Texto de la declaración bancaria (máx. 22 chars) |
| tax_code | string | No | Stripe Código fiscal |
| price.amount | number | No | Precio en unidad de divisas más pequeña (centros) |
| price.currency | string | No | ISO 4217 código de divisas (por ejemplo, "usd", "jmd") |
| price.tax_behavior | string | No | inclusivo, exclusivo o no especificado |
Clientes
Administrar registros de clientes para compras repetidas y suscripciones.
| Método | Ruta | Descripción |
|---|---|---|
| POST | /v1/customers | Crear un cliente |
| GET | /v1/customers | Lista de clientes |
| GET | /v1/customers/:id | Consigue un cliente |
| PUT | /v1/customers/:id | Actualizar un cliente |
| DELETE | /v1/customers/:id | Eliminar un cliente |
Crear un cliente
const customer = await handypay("/customers", {
method: "POST",
body: JSON.stringify({
email: "customer@example.com",
name: "Jane Doe",
}),
});
console.log(customer.id); // "cus_abc123"ejemplo de 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"
}'Solicitud body
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| string | Sí. | Correo electrónico al cliente | |
| name | string | No | Nombre del cliente |
| phone | string | No | Número de teléfono del cliente |
| metadata | object | No | Parejas de valor clave personalizado |
Sesiones de pago
Cree sesiones de pago alojadas para pagos únicos. Se aplica el precio estándar de HandyPay: 4,9 % + 0,40 USD por transacción en el plan gratuito, o 4,2 % + 0,40 USD en el plan Pro. No hay API adicional ni tarifa de plataforma adicional.
| Método | Ruta | Descripción |
|---|---|---|
| POST | /v1/payment-sessions | Crear una sesión de pago |
| GET | /v1/payment-sessions/:id | Obtenga el estado de la sesión |
| GET | /v1/test-payments | Pagos de prueba de lista (hp_test_ only) |
Crear una sesión de pago (con precio 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;ejemplo de 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"
}'Crear una sesión de pago (cantidad 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",
}),
});ejemplo de 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"
}'Solicitud body
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| line_items | array | Sí. | Al menos un artículo de línea |
| success_url | string | Sí. | Redirect URL después de pago exitoso |
| cancel_url | string | Sí. | Redirect URL si el cliente cancela |
| customer_id | string | No | Cliente existente ID |
| customer_email | string | No | Completar previamente el correo (si no hay customer_id) |
| pass_fees_to_customer | boolean | No | Añadir las tasas de procesamiento y servicio al total del cliente |
| metadata | object | No | Parejas de valor clave personalizado |
| collect_shipping_address | boolean | No | Recoger una dirección de envío |
| billing_address_collection | string | No | auto o requerido |
| shipping_countries | string[] | No | Permitidos países de destino de ISO-2 |
| shipping_options | array | No | Etiquetas y cantidades de envío en la unidad de divisas más pequeña |
Campos de partidas individuales
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| price_id | string | No | Stripe Price ID |
| amount | number | No | Cantidad a la medida en centavos |
| currency | string | No | Necesario con cantidad |
| name | string | No | Necesario con cantidad |
| quantity | number | Sí. | Cantidad |
Proporcionar ya sea price_id o amount+currency+name por línea de pedido.
{CHECKOUT_SESSION_ID} en la URL de éxito. Consulta esa ID con el mismo comerciante y el mismo modo de clave en vivo/de prueba que la creó. La sesión sigue siendo consultable una vez finalizada, pero su webhook firmado aún debería impulsar el cumplimiento de la factura.Pagos integrados
Cree un PaymentIntent desde su servidor cuando desee representar Stripe Elements en su propia página de pago. Su clave API HandyPay permanece en el lado del servidor; envíe solo la clave publicable devuelta, el ID de la cuenta conectada y el secreto de cliente de corta duración al navegador.
| Método | Ruta | Descripción |
|---|---|---|
| POST | /v1/payment-intents | Crear un 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 | Descripción |
|---|---|---|---|
| amount | number | Sí. | entero positivo en la unidad de divisas más pequeña |
| currency | string | Sí. | ISO 4217 código de moneda de tres letras |
| description | string | No | Descripción del pago |
| customer_email | string | No | Email del cliente para recibos y reconciliación |
| pass_fees_to_customer | boolean | No | Averiguar la cantidad para que el cliente cubra los honorarios |
| metadata | object | No | Su orden o identificadores de factura |
Reembolsos
Reembolso de un pago propiedad del comerciante autenticado. HandyPay verifica la propiedad del pago y el saldo disponible antes de crear la reversión. Omita amount para obtener un reembolso completo.
| Método | Ruta | Descripción |
|---|---|---|
| POST | /v1/refunds | Crear un reembolso total o 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 | Descripción |
|---|---|---|---|
| session_id | string | condicional | ID de Checkout Session (cs_...). Use este campo o payment_intent |
| payment_intent | string | condicional | ID de PaymentIntent (pi_...). Use este campo o session_id |
| amount | number | No | Cantidad de reembolso parcial en la unidad de divisa más pequeña |
| reason | string | No | duplicate, fraudulent o requested_by_customer |
Un pago disputado, reembolsado en su totalidad, entre comerciantes o con saldo insuficiente se rechaza sin crear un reembolso.
Disputas
Revisar devoluciones de cargo para al comerciante conectado y proporcionar evidencia antes de la fecha de vencimiento.
| Método | Ruta | Descripción |
|---|---|---|
| GET | /v1/disputes | Distinciones de listas |
| GET | /v1/disputes/:id | Obtenga una disputa y su estado de evidencia |
| POST | /v1/disputes/:id/evidence | Actualizar o presentar pruebas |
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 | Descripción |
|---|---|---|---|
| evidence | object | No | Stripe disputa campos de evidencia como valores de cadena |
| submit | boolean | No | Es verdad sólo cuando la evidencia está completa y lista para su revisión |
Suscripciones
Crear productos recurrentes y administrar suscripciones.
Productos de suscripción
| Método | Ruta | Descripción |
|---|---|---|
| POST | /v1/subscription-products | Crear un producto de suscripción |
| GET | /v1/subscription-products | Productos de suscripción |
Sesiones y administración de suscripciones
| Método | Ruta | Descripción |
|---|---|---|
| POST | /v1/subscription-sessions | Crear chequeo de suscripción |
| GET | /v1/subscriptions | Lista de suscripciones activas |
| PATCH | /v1/subscriptions/:id/quantity | Cambio de asientos con prorración explícita |
| POST | /v1/subscriptions/:id/cancel | Cancelación al final del período de facturación |
Intervalos de facturación admitidos
Crear un producto de suscripción
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 sessionsejemplo de 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
}'Solicitud body
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| name | string | Sí. | Nombre del producto |
| description | string | No | Descripción del producto |
| amount | number | Sí. | Precio en unidad de divisas más pequeña |
| currency | string | Sí. | ISO 4217 código de moneda |
| interval | string | Sí. | Uno de los intervalos soportados |
| trial_period_days | number | No | Duración del juicio libre en días |
| metadata | object | No | Parejas de valor clave personalizado |
Crear pago con varios puestos
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",
}),
});Cambiar puestos en una suscripción activa
La cantidad debe ser un número entero de 1 a 1000. Elija cómo se maneja el ajuste de facturación en lugar de depender de un valor predeterminado implícito.
const updated = await handypay("/subscriptions/sub_123/quantity", {
method: "PATCH",
body: JSON.stringify({
quantity: 8,
proration_behavior: "create_prorations",
}),
});| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| quantity | number | Sí. | Nuevos asientos cuentan de 1 a 1.000 |
| proration_behavior | string | No | create_prorations, always_invoice o none |
| item_id | string | No | Artículo de suscripción específico cuando una suscripción tiene múltiples productos |
Webhooks
Reciba notificaciones de eventos en tiempo real a través de HTTP POST a sus puntos finales.
| Método | Ruta | Descripción |
|---|---|---|
| POST | /v1/webhook-endpoints | Registrar un punto final |
| GET | /v1/webhook-endpoints | Puntos finales de lista |
| DELETE | /v1/webhook-endpoints/:id | Desactivar un punto final |
- Los puntos finales deben usar HTTPS.
- Los puntos finales de Webhook registrados con una
hp_test_clave recibe eventos de prueba únicamente. Los destinos en vivo y de prueba se almacenan por separado. - Cada punto final activo suscrito a un evento recibe una entrega independiente firmada con el secreto propio de ese punto final. No reutilice el secreto de un punto final para otro.
- Devuelva una respuesta 2xx en 10 segundos. Almacene cada evento
idantes de los efectos secundarios para que las entregas duplicadas sean seguras. - Después de 10 errores de entrega consecutivos, un punto final se desactiva automáticamente.
Tipos de eventos admitidos
- 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 evento identifica el PaymentIntent en data.id; utilice sus metadatos (por ejemplo order_id) para conciliarlos. Utilice checkout.session.expired para la caducidad de la sesión y los eventos asíncronos para métodos de pago retrasados.Verificación de firmas de webhook
Cada entrega incluye un X-HandyPay-Signature encabezado en el formato sha256={hex}. Verifique calculando HMAC-SHA256 del cuerpo de la solicitud sin procesar utilizando el secreto de firma de su punto final:
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);
}Manejador de ruta de API de 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 });
}Manejador 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 de Webhook
{
"id": "evt_abc123",
"type": "payment_intent.succeeded",
"created": 1706745600,
"data": { ... }
}Cuenta
Lea el cargo del comerciante conectado y la disposición de pago más la cuenta bancaria de pago predeterminada. Los números de banco y de ruta están enmascarados; sólo se devuelven sus últimos cuatro dígitos.
| Método | Ruta | Descripción |
|---|---|---|
| GET | /v1/account | Obtener cuenta conectada y detalles de pago enmascarados |
const account = await handypay("/account");
console.log({
chargesEnabled: account.chargesEnabled,
payoutsEnabled: account.payoutsEnabled,
bank: account.bankAccount?.bankName,
last4: account.bankAccount?.last4,
});Códigos de error
| Código | HTTP | Descripción |
|---|---|---|
| unauthorized | 401 | Falta o no llave de API |
| key_revoked | 401 | API Key ha sido revocado |
| key_expired | 401 | API Key ha expirado |
| rate_limit_exceeded | 429 | Demasiadas solicitudes |
| validation_error | 400 | Solicitud de validación corporal fallido |
| invalid_url | 400 | success_url o cancel_url no válidos |
| invalid_interval | 400 | Intervalo de facturación sin apoyo |
| product_not_found | 404 | El producto no existe |
| customer_not_found | 404 | El cliente no existe |
| session_not_found | 404 | No existe la sesión de verificación |
| payment_not_found | 404 | El pago falta o no es propiedad de este comerciante |
| subscription_not_found | 404 | La suscripción no existe |
| refund_not_allowed | 400 | El pago es disputado y no se puede devolver |
| already_refunded | 400 | El pago ya se reembolsa completamente |
| refund_amount_too_large | 400 | El importe excede el saldo reembolsable restante |
| insufficient_balance | 400 | El saldo disponible no puede cubrir el reembolso |
| balance_verification_failed | 400 | No se puede verificar la propiedad o la disponibilidad de equilibrio |
| endpoint_not_found | 404 | Desconocido API endpoint |
| stripe_error | 502 | Stripe API devolvió un error |
| internal_error | 500 | Error de servidor no esperado |
| payload_too_large | 413 | Cuerpo de solicitud excede 1MB |
| prohibited_content | 400 | El contenido viola la política de uso aceptable |
| webhook_url_must_be_https | 400 | Webhook URL debe utilizar HTTPS |
| key_creation_rate_exceeded | 429 | Demasiados llaves creadas en la ventana del tiempo |
| max_keys_reached | 400 | Máximas teclas de API activas alcanzadas (25) |
Seguridad
- Mantenga las claves
hp_live_yhp_test_en su servidor. Nunca los coloque en JavaScript del navegador, aplicaciones móviles, URL, registros o control de fuente. - Todas las respuestas incluyen
X-Content-Type-Options: nosniff,X-Frame-Options: DENYyStrict-Transport-Securityencabezados. - Límite de cuerpo de solicitud: 1 MiB, incluidas solicitudes transmitidas o fragmentadas.
- Los puntos finales de webhook deben usar HTTPS.
- Verifique las firmas de webhook con los bytes sin procesar exactos y compare resúmenes en tiempo constante.
- Los nombres y descripciones de los productos se comparan con un contenido prohibido lista de bloqueo.
- Más de 5 infracciones de contenido en 24 horas suspenderán tus claves API.
Límites de claves API
- Máximo 3 claves creadas por ventana de 10 minutos.
- Máximo 10 claves creadas por ventana de 1 hora.
- Máximo 25 claves activas por comerciante.