Referencia de API

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/v1
bash

Versió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
javascript
  • 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.

Listar pagos de prueba
curl https://api.handypay.me/api/v1/test-payments \
  -H "Authorization: Bearer hp_test_your_api_key_here"
bash

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

JSON
{
  "success": true,
  "data": { ... },
  "request_id": "550e8400-e29b-41d4-a716-446655440000"
}
json

Error

JSON
{
  "success": false,
  "error": {
    "code": "validation_error",
    "message": "Name is required"
  },
  "request_id": "550e8400-e29b-41d4-a716-446655440000"
}
json

Paginación

Todos los puntos finales de la lista utilizan paginación basada en cursor.

CampoTipoRequeridoDescripción
limitnumberNoArtículos por página (1–100, por defecto 10)
starting_afterstringNoID 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étodoRutaDescripción
POST/v1/productsCrear un producto
GET/v1/productsLista de productos
GET/v1/products/:idConsiga un producto
PUT/v1/products/:idActualizar un producto
DELETE/v1/products/:idArchivo de un producto

Crear un producto

TypeScript
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"
typescript
ejemplo de cURL
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"
    }
  }'
bash

Solicitud body

CampoTipoRequeridoDescripción
namestringSí.Nombre del producto
descriptionstringNoDescripción del producto
imagesstring[]NoHasta 8 URLs de imagen
metadataobjectNoParejas de valor clave personalizadas (hasta 50 teclas)
activebooleanNoDefault true
urlstringNoPágina de producto URL en su sitio
shippablebooleanNoSi el producto requiere envío
unit_labelstringNoEtiqueta por unidad (por ejemplo. "sello", "license"
statement_descriptorstringNoTexto de la declaración bancaria (máx. 22 chars)
tax_codestringNoStripe Código fiscal
price.amountnumberNoPrecio en unidad de divisas más pequeña (centros)
price.currencystringNoISO 4217 código de divisas (por ejemplo, "usd", "jmd")
price.tax_behaviorstringNoinclusivo, exclusivo o no especificado

Clientes

Administrar registros de clientes para compras repetidas y suscripciones.

MétodoRutaDescripción
POST/v1/customersCrear un cliente
GET/v1/customersLista de clientes
GET/v1/customers/:idConsigue un cliente
PUT/v1/customers/:idActualizar un cliente
DELETE/v1/customers/:idEliminar un cliente

Crear un cliente

TypeScript
const customer = await handypay("/customers", {
  method: "POST",
  body: JSON.stringify({
    email: "customer@example.com",
    name: "Jane Doe",
  }),
});

console.log(customer.id); // "cus_abc123"
typescript
ejemplo de cURL
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"
  }'
bash

Solicitud body

CampoTipoRequeridoDescripción
emailstringSí.Correo electrónico al cliente
namestringNoNombre del cliente
phonestringNoNúmero de teléfono del cliente
metadataobjectNoParejas 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étodoRutaDescripción
POST/v1/payment-sessionsCrear una sesión de pago
GET/v1/payment-sessions/:idObtenga el estado de la sesión
GET/v1/test-paymentsPagos de prueba de lista (hp_test_ only)

Crear una sesión de pago (con precio existente)

TypeScript
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;
typescript
ejemplo de cURL
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"
  }'
bash

Crear una sesión de pago (cantidad personalizada)

TypeScript
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",
  }),
});
typescript
ejemplo de cURL
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"
  }'
bash

Solicitud body

CampoTipoRequeridoDescripción
line_itemsarraySí.Al menos un artículo de línea
success_urlstringSí.Redirect URL después de pago exitoso
cancel_urlstringSí.Redirect URL si el cliente cancela
customer_idstringNoCliente existente ID
customer_emailstringNoCompletar previamente el correo (si no hay customer_id)
pass_fees_to_customerbooleanNoAñadir las tasas de procesamiento y servicio al total del cliente
metadataobjectNoParejas de valor clave personalizado
collect_shipping_addressbooleanNoRecoger una dirección de envío
billing_address_collectionstringNoauto o requerido
shipping_countriesstring[]NoPermitidos países de destino de ISO-2
shipping_optionsarrayNoEtiquetas y cantidades de envío en la unidad de divisas más pequeña

Campos de partidas individuales

CampoTipoRequeridoDescripción
price_idstringNoStripe Price ID
amountnumberNoCantidad a la medida en centavos
currencystringNoNecesario con cantidad
namestringNoNecesario con cantidad
quantitynumberSí.Cantidad

Proporcionar ya sea price_id o amount+currency+name por línea de pedido.

sustitutos de HandyPay {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étodoRutaDescripción
POST/v1/payment-intentsCrear un PaymentIntent integrado
TypeScript del lado del servidor
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,
};
typescript
CampoTipoRequeridoDescripción
amountnumberSí.entero positivo en la unidad de divisas más pequeña
currencystringSí.ISO 4217 código de moneda de tres letras
descriptionstringNoDescripción del pago
customer_emailstringNoEmail del cliente para recibos y reconciliación
pass_fees_to_customerbooleanNoAveriguar la cantidad para que el cliente cubra los honorarios
metadataobjectNoSu 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étodoRutaDescripción
POST/v1/refundsCrear un reembolso total o parcial
TypeScript
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);
typescript
CampoTipoRequeridoDescripción
session_idstringcondicionalID de Checkout Session (cs_...). Use este campo o payment_intent
payment_intentstringcondicionalID de PaymentIntent (pi_...). Use este campo o session_id
amountnumberNoCantidad de reembolso parcial en la unidad de divisa más pequeña
reasonstringNoduplicate, 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étodoRutaDescripción
GET/v1/disputesDistinciones de listas
GET/v1/disputes/:idObtenga una disputa y su estado de evidencia
POST/v1/disputes/:id/evidenceActualizar o presentar pruebas
TypeScript
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,
  }),
});
typescript
CampoTipoRequeridoDescripción
evidenceobjectNoStripe disputa campos de evidencia como valores de cadena
submitbooleanNoEs 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étodoRutaDescripción
POST/v1/subscription-productsCrear un producto de suscripción
GET/v1/subscription-productsProductos de suscripción

Sesiones y administración de suscripciones

MétodoRutaDescripción
POST/v1/subscription-sessionsCrear chequeo de suscripción
GET/v1/subscriptionsLista de suscripciones activas
PATCH/v1/subscriptions/:id/quantityCambio de asientos con prorración explícita
POST/v1/subscriptions/:id/cancelCancelación al final del período de facturación

Intervalos de facturación admitidos

weeklybi-weeklymonthlybi-monthlyquarterlysemi-annualannual

Crear un producto de suscripción

TypeScript
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 sessions
typescript
ejemplo de cURL
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
  }'
bash

Solicitud body

CampoTipoRequeridoDescripción
namestringSí.Nombre del producto
descriptionstringNoDescripción del producto
amountnumberSí.Precio en unidad de divisas más pequeña
currencystringSí.ISO 4217 código de moneda
intervalstringSí.Uno de los intervalos soportados
trial_period_daysnumberNoDuración del juicio libre en días
metadataobjectNoParejas de valor clave personalizado

Crear pago con varios puestos

TypeScript
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",
  }),
});
typescript

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.

TypeScript
const updated = await handypay("/subscriptions/sub_123/quantity", {
  method: "PATCH",
  body: JSON.stringify({
    quantity: 8,
    proration_behavior: "create_prorations",
  }),
});
typescript
CampoTipoRequeridoDescripción
quantitynumberSí.Nuevos asientos cuentan de 1 a 1.000
proration_behaviorstringNocreate_prorations, always_invoice o none
item_idstringNoArtí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étodoRutaDescripción
POST/v1/webhook-endpointsRegistrar un punto final
GET/v1/webhook-endpointsPuntos finales de lista
DELETE/v1/webhook-endpoints/:idDesactivar 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 id antes 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
Trate los webhooks firmados como la fuente verdadera del pago, no como la redirección exitosa del navegador. Un 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:

TypeScript
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);
}
typescript

Manejador de ruta de API de Next.js

app/api/webhooks/handypay/route.ts
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 });
}
typescript
Manejador Express.js
routes/webhooks.ts
// 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;
typescript

Formato de carga útil de Webhook

JSON
{
  "id": "evt_abc123",
  "type": "payment_intent.succeeded",
  "created": 1706745600,
  "data": { ... }
}
json

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étodoRutaDescripción
GET/v1/accountObtener cuenta conectada y detalles de pago enmascarados
TypeScript
const account = await handypay("/account");

console.log({
  chargesEnabled: account.chargesEnabled,
  payoutsEnabled: account.payoutsEnabled,
  bank: account.bankAccount?.bankName,
  last4: account.bankAccount?.last4,
});
typescript

Códigos de error

CódigoHTTPDescripción
unauthorized401Falta o no llave de API
key_revoked401API Key ha sido revocado
key_expired401API Key ha expirado
rate_limit_exceeded429Demasiadas solicitudes
validation_error400Solicitud de validación corporal fallido
invalid_url400success_url o cancel_url no válidos
invalid_interval400Intervalo de facturación sin apoyo
product_not_found404El producto no existe
customer_not_found404El cliente no existe
session_not_found404No existe la sesión de verificación
payment_not_found404El pago falta o no es propiedad de este comerciante
subscription_not_found404La suscripción no existe
refund_not_allowed400El pago es disputado y no se puede devolver
already_refunded400El pago ya se reembolsa completamente
refund_amount_too_large400El importe excede el saldo reembolsable restante
insufficient_balance400El saldo disponible no puede cubrir el reembolso
balance_verification_failed400No se puede verificar la propiedad o la disponibilidad de equilibrio
endpoint_not_found404Desconocido API endpoint
stripe_error502Stripe API devolvió un error
internal_error500Error de servidor no esperado
payload_too_large413Cuerpo de solicitud excede 1MB
prohibited_content400El contenido viola la política de uso aceptable
webhook_url_must_be_https400Webhook URL debe utilizar HTTPS
key_creation_rate_exceeded429Demasiados llaves creadas en la ventana del tiempo
max_keys_reached400Máximas teclas de API activas alcanzadas (25)

Seguridad

  • Mantenga las claves hp_live_ y hp_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: DENY y Strict-Transport-Security encabezados.
  • 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.