Complete endpoint reference with TypeScript examples. All examples use the handypay helper from the Quick Start.
Base URL
https://api.handypay.me/api/v1API 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- 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
{
"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"
}Pagination
All list endpoints use cursor-based pagination.
| Field | Type | Required | Description |
|---|---|---|---|
| limit | number | No | Items per page (1–100, default 10) |
| starting_after | string | No | ID 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.
| Method | Path | Description |
|---|---|---|
| POST | /v1/products | Create a product |
| GET | /v1/products | List products |
| GET | /v1/products/:id | Get a product |
| PUT | /v1/products/:id | Update a product |
| DELETE | /v1/products/:id | Archive a product |
Create a product
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"cURL example
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"
}
}'Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Product name |
| description | string | No | Product description |
| images | string[] | No | Up to 8 image URLs |
| metadata | object | No | Custom key-value pairs (up to 50 keys) |
| active | boolean | No | Default true |
| url | string | No | Product page URL on your site |
| shippable | boolean | No | Whether product requires shipping |
| unit_label | string | No | Per-unit label (e.g. "seat", "license") |
| statement_descriptor | string | No | Bank statement text (max 22 chars) |
| tax_code | string | No | Stripe Tax code |
| price.amount | number | No | Price in smallest currency unit (cents) |
| price.currency | string | No | ISO 4217 currency code (e.g. "usd", "jmd") |
| price.tax_behavior | string | No | inclusive, exclusive, or unspecified |
Customers
Manage customer records for repeat purchases and subscriptions.
| Method | Path | Description |
|---|---|---|
| POST | /v1/customers | Create a customer |
| GET | /v1/customers | List customers |
| GET | /v1/customers/:id | Get a customer |
| PUT | /v1/customers/:id | Update a customer |
| DELETE | /v1/customers/:id | Delete a customer |
Create a customer
const customer = await handypay("/customers", {
method: "POST",
body: JSON.stringify({
email: "[email protected]",
name: "Jane Doe",
}),
});
console.log(customer.id); // "cus_abc123"cURL example
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"
}'Request body
| Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | Customer email | |
| name | string | No | Customer name |
| phone | string | No | Customer phone number |
| metadata | object | No | Custom key-value pairs |
Payment Sessions
Create hosted checkout sessions for one-time payments. A platform fee of 2% + $0.10 is applied.
| Method | Path | Description |
|---|---|---|
| POST | /v1/payment-sessions | Create a payment session |
| GET | /v1/payment-sessions/:id | Get session status |
Create a payment session (with existing price)
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;cURL example
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"
}'Create a payment session (custom amount)
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",
}),
});cURL example
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"
}'Request body
| Field | Type | Required | Description |
|---|---|---|---|
| line_items | array | Yes | At least one line item |
| success_url | string | Yes | Redirect URL after successful payment |
| cancel_url | string | Yes | Redirect URL if customer cancels |
| customer_id | string | No | Existing customer ID |
| customer_email | string | No | Pre-fill email (if no customer_id) |
| metadata | object | No | Custom key-value pairs |
Line item fields
| Field | Type | Required | Description |
|---|---|---|---|
| price_id | string | No | Existing Stripe Price ID |
| amount | number | No | Custom amount in cents |
| currency | string | No | Required with amount |
| name | string | No | Required with amount |
| quantity | number | Yes | Quantity |
Provide either price_id or amount+currency+name per line item.
Subscriptions
Create recurring products and manage subscriptions.
Subscription Products
| Method | Path | Description |
|---|---|---|
| POST | /v1/subscription-products | Create a subscription product |
| GET | /v1/subscription-products | List subscription products |
Subscription Sessions & Management
| Method | Path | Description |
|---|---|---|
| POST | /v1/subscription-sessions | Create subscription checkout |
| GET | /v1/subscriptions | List active subscriptions |
| POST | /v1/subscriptions/:id/cancel | Cancel at end of billing period |
Supported billing intervals
Create a subscription product
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 sessionscURL example
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
}'Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Product name |
| description | string | No | Product description |
| amount | number | Yes | Price in smallest currency unit |
| currency | string | Yes | ISO 4217 currency code |
| interval | string | Yes | One of the supported intervals |
| trial_period_days | number | No | Free trial duration in days |
| metadata | object | No | Custom key-value pairs |
Webhooks
Receive real-time event notifications via HTTP POST to your endpoints.
| Method | Path | Description |
|---|---|---|
| POST | /v1/webhook-endpoints | Register an endpoint |
| GET | /v1/webhook-endpoints | List endpoints |
| DELETE | /v1/webhook-endpoints/:id | Deactivate 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:
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}`;
}Next.js API Route handler
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 });
}Express.js handler
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;Webhook payload format
{
"id": "evt_abc123",
"type": "payment_intent.succeeded",
"created": 1706745600,
"data": { ... }
}Error Codes
| Code | HTTP | Description |
|---|---|---|
| unauthorized | 401 | Missing or invalid API key |
| key_revoked | 401 | API key has been revoked |
| key_expired | 401 | API key has expired |
| rate_limit_exceeded | 429 | Too many requests |
| validation_error | 400 | Request body validation failed |
| invalid_url | 400 | Invalid success_url or cancel_url |
| invalid_interval | 400 | Unsupported billing interval |
| product_not_found | 404 | Product does not exist |
| customer_not_found | 404 | Customer does not exist |
| session_not_found | 404 | Checkout session does not exist |
| subscription_not_found | 404 | Subscription does not exist |
| endpoint_not_found | 404 | Unknown API endpoint |
| stripe_error | 502 | Stripe API returned an error |
| internal_error | 500 | Unexpected server error |
| payload_too_large | 413 | Request body exceeds 1MB |
| prohibited_content | 400 | Content violates acceptable use policy |
| webhook_url_must_be_https | 400 | Webhook URL must use HTTPS |
| key_creation_rate_exceeded | 429 | Too many keys created in time window |
| max_keys_reached | 400 | Maximum active API keys reached (25) |
Security
- All responses include
X-Content-Type-Options: nosniff,X-Frame-Options: DENY, andStrict-Transport-Securityheaders. - 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.