HandyPay
HandyPay

API Reference

Complete endpoint reference with TypeScript examples. All examples use the handypay helper from the Quick Start.

Base URL

https://api.handypay.me/api/v1
bash

API version: 2025-01-01 (returned in X-API-Version header on every response).

Authentication

All requests require a Bearer token in the Authorization header.

Authorization: Bearer hp_live_your_api_key_here
javascript
  • Keys prefixed with hp_live_ are for production.
  • Keys prefixed with hp_test_ are for sandbox/testing.
  • Generate and manage keys from the Merchant Portal.

Rate Limits

1,000 requests per hour per API key. Exceeding the limit returns 429 with a Retry-After header.

Response Format

Every response is wrapped in a standard envelope.

Success

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

Pagination

All list endpoints use cursor-based pagination.

FieldTypeRequiredDescription
limitnumberNoItems per page (1–100, default 10)
starting_afterstringNoID of the last item from previous page

Response includes has_more: true when additional pages exist.

Products

Create and manage products for one-time purchases.

MethodPathDescription
POST/v1/productsCreate a product
GET/v1/productsList products
GET/v1/products/:idGet a product
PUT/v1/products/:idUpdate a product
DELETE/v1/products/:idArchive a product

Create a product

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
cURL example
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

Request body

FieldTypeRequiredDescription
namestringYesProduct name
descriptionstringNoProduct description
imagesstring[]NoUp to 8 image URLs
metadataobjectNoCustom key-value pairs (up to 50 keys)
activebooleanNoDefault true
urlstringNoProduct page URL on your site
shippablebooleanNoWhether product requires shipping
unit_labelstringNoPer-unit label (e.g. "seat", "license")
statement_descriptorstringNoBank statement text (max 22 chars)
tax_codestringNoStripe Tax code
price.amountnumberNoPrice in smallest currency unit (cents)
price.currencystringNoISO 4217 currency code (e.g. "usd", "jmd")
price.tax_behaviorstringNoinclusive, exclusive, or unspecified

Customers

Manage customer records for repeat purchases and subscriptions.

MethodPathDescription
POST/v1/customersCreate a customer
GET/v1/customersList customers
GET/v1/customers/:idGet a customer
PUT/v1/customers/:idUpdate a customer
DELETE/v1/customers/:idDelete a customer

Create a customer

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

console.log(customer.id); // "cus_abc123"
typescript
cURL example
cURL
curl -X POST https://api.handypay.me/api/v1/customers \
  -H "Authorization: Bearer hp_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "name": "Jane Doe"
  }'
bash

Request body

FieldTypeRequiredDescription
emailstringYesCustomer email
namestringNoCustomer name
phonestringNoCustomer phone number
metadataobjectNoCustom key-value pairs

Payment Sessions

Create hosted checkout sessions for one-time payments. A platform fee of 2% + $0.10 is applied.

MethodPathDescription
POST/v1/payment-sessionsCreate a payment session
GET/v1/payment-sessions/:idGet session status

Create a payment session (with existing price)

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",
    cancel_url: "https://yoursite.com/cancel",
  }),
});

// Redirect customer to checkout
window.location.href = session.url;
typescript
cURL example
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",
    "cancel_url": "https://yoursite.com/cancel"
  }'
bash

Create a payment session (custom amount)

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",
    cancel_url: "https://yoursite.com/cancel",
  }),
});
typescript
cURL example
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",
    "cancel_url": "https://yoursite.com/cancel"
  }'
bash

Request body

FieldTypeRequiredDescription
line_itemsarrayYesAt least one line item
success_urlstringYesRedirect URL after successful payment
cancel_urlstringYesRedirect URL if customer cancels
customer_idstringNoExisting customer ID
customer_emailstringNoPre-fill email (if no customer_id)
metadataobjectNoCustom key-value pairs

Line item fields

FieldTypeRequiredDescription
price_idstringNoExisting Stripe Price ID
amountnumberNoCustom amount in cents
currencystringNoRequired with amount
namestringNoRequired with amount
quantitynumberYesQuantity

Provide either price_id or amount+currency+name per line item.

Subscriptions

Create recurring products and manage subscriptions.

Subscription Products

MethodPathDescription
POST/v1/subscription-productsCreate a subscription product
GET/v1/subscription-productsList subscription products

Subscription Sessions & Management

MethodPathDescription
POST/v1/subscription-sessionsCreate subscription checkout
GET/v1/subscriptionsList active subscriptions
POST/v1/subscriptions/:id/cancelCancel at end of billing period

Supported billing intervals

weeklybi-weeklymonthlybi-monthlyquarterlysemi-annualannual

Create a subscription product

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
cURL example
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

Request body

FieldTypeRequiredDescription
namestringYesProduct name
descriptionstringNoProduct description
amountnumberYesPrice in smallest currency unit
currencystringYesISO 4217 currency code
intervalstringYesOne of the supported intervals
trial_period_daysnumberNoFree trial duration in days
metadataobjectNoCustom key-value pairs

Webhooks

Receive real-time event notifications via HTTP POST to your endpoints.

MethodPathDescription
POST/v1/webhook-endpointsRegister an endpoint
GET/v1/webhook-endpointsList endpoints
DELETE/v1/webhook-endpoints/:idDeactivate an endpoint
  • Endpoints must use HTTPS.
  • After 10 consecutive delivery failures, an endpoint is automatically deactivated.

Supported event types

  • payment_intent.succeeded
  • payment_intent.payment_failed
  • checkout.session.completed
  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted

Verifying webhook signatures

Each delivery includes an X-HandyPay-Signature header in the format sha256={hex}. Verify by computing HMAC-SHA256 of the raw request body using your endpoint's signing secret:

TypeScript
import crypto from "crypto";

function verifySignature(
  payload: string,
  secret: string,
  signature: string
): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
  return signature === `sha256=${expected}`;
}
typescript

Next.js API Route handler

app/api/webhooks/handypay/route.ts
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";

const WEBHOOK_SECRET = process.env.HANDYPAY_WEBHOOK_SECRET!;

export async function POST(req: NextRequest) {
  const body = await req.text();
  const signature = req.headers.get("x-handypay-signature") ?? "";

  const expected = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(body)
    .digest("hex");

  if (signature !== `sha256=${expected}`) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }

  const event = JSON.parse(body);

  switch (event.type) {
    case "checkout.session.completed":
      // Fulfill the order
      break;
    case "payment_intent.payment_failed":
      // Notify the customer
      break;
    case "customer.subscription.created":
      // Activate the subscription
      break;
    case "customer.subscription.deleted":
      // Revoke access
      break;
  }

  return NextResponse.json({ received: true });
}
typescript
Express.js handler
routes/webhooks.ts
import express from "express";
import crypto from "crypto";

const router = express.Router();
const WEBHOOK_SECRET = process.env.HANDYPAY_WEBHOOK_SECRET!;

router.post(
  "/handypay",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.headers["x-handypay-signature"] as string;
    const expected = crypto
      .createHmac("sha256", WEBHOOK_SECRET)
      .update(req.body)
      .digest("hex");

    if (signature !== `sha256=${expected}`) {
      return res.status(401).json({ error: "Invalid signature" });
    }

    const event = JSON.parse(req.body.toString());
    console.log(`Received ${event.type}`, event.data);

    res.json({ received: true });
  }
);

export default router;
typescript

Webhook payload format

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

Error Codes

CodeHTTPDescription
unauthorized401Missing or invalid API key
key_revoked401API key has been revoked
key_expired401API key has expired
rate_limit_exceeded429Too many requests
validation_error400Request body validation failed
invalid_url400Invalid success_url or cancel_url
invalid_interval400Unsupported billing interval
product_not_found404Product does not exist
customer_not_found404Customer does not exist
session_not_found404Checkout session does not exist
subscription_not_found404Subscription does not exist
endpoint_not_found404Unknown API endpoint
stripe_error502Stripe API returned an error
internal_error500Unexpected server error
payload_too_large413Request body exceeds 1MB
prohibited_content400Content violates acceptable use policy
webhook_url_must_be_https400Webhook URL must use HTTPS
key_creation_rate_exceeded429Too many keys created in time window
max_keys_reached400Maximum active API keys reached (25)

Security

  • All responses include X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and Strict-Transport-Security headers.
  • Request body limit: 1MB.
  • Webhook endpoints must use HTTPS.
  • Product names and descriptions are screened against a prohibited content blocklist.
  • 5+ content violations in 24 hours will suspend your API keys.

API Key Limits

  • Max 3 keys created per 10-minute window.
  • Max 10 keys created per 1-hour window.
  • Max 25 active keys per merchant.