Référence complète du paramètre avec des exemples de TypeScript. Tous les exemples utilisent le handypay aide de la Démarrer rapidement- Oui. L'outil peut télécharger le Contrat OpenAPI 3.1.
Base URL
https://api.handypay.me/api/v1Version API: 2025-01-01 (retourné X-API-Version en-tête sur chaque réponse).
Authentification
Toutes les demandes nécessitent un jeton porteur dans le Authorization Entête.
Authorization: Bearer hp_live_your_api_key_here- Clés préfixées avec
hp_live_sont destinés à la production. - Clés préfixées avec
hp_test_sont pour sandbox/testing. - Générer et gérer les clés à partir de la Portail marchand.
Le mode d'essai est complètement isolé
Tous les hp_test_ demande utilise un compte de test dédié. Les produits de test, les clients, les paiements, les abonnements et les paramètres de webhook ne sont jamais apparus dans l'activité en direct. Voir dans le Espace de travail du mode d'essai.
curl https://api.handypay.me/api/v1/test-payments \
-H "Authorization: Bearer hp_test_your_api_key_here"Limites de taux
1000 demandes par heure par clé API. Dépassant les limites de rendement 429 avec une Retry-After Entête.
Format de réponse
Chaque réponse est enveloppée dans une enveloppe standard.
Succès
{
"success": true,
"data": { ... },
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}Erreur
{
"success": false,
"error": {
"code": "validation_error",
"message": "Name is required"
},
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}Pagination
Tous les paramètres de la liste utilisent la pagination par curseur.
| Champ | Type | Requis | Désignation des marchandises |
|---|---|---|---|
| limit | number | Numéro | Articles par page (1–100, par défaut 10) |
| starting_after | string | Numéro | ID du dernier article de la page précédente |
La réponse comprend has_more: true lorsque des pages supplémentaires existent.
Produits
Créer et gérer des produits pour des achats ponctuels.
| Méthode | Voie | Désignation des marchandises |
|---|---|---|
| POST | /v1/products | Créer un produit |
| GET | /v1/products | Dénomination des produits |
| GET | /v1/products/:id | Obtenez un produit |
| PUT | /v1/products/:id | Mettre à jour un produit |
| DELETE | /v1/products/:id | Archiver un produit |
Créer un produit
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"exemple 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"
}
}'Organisme de demande
| Champ | Type | Requis | Désignation des marchandises |
|---|---|---|---|
| name | string | Oui | Nom du produit |
| description | string | Numéro | Description du produit |
| images | string[] | Numéro | Jusqu'à 8 URLs d'images |
| metadata | object | Numéro | Paires personnalisées de valeurs de clés (jusqu'à 50 clés) |
| active | boolean | Numéro | Par défaut vrai |
| url | string | Numéro | Page produit URL sur votre site |
| shippable | boolean | Numéro | Indique si le produit nécessite une expédition |
| unit_label | string | Numéro | Étiquette par unité (p. ex. "siège", "licence") |
| statement_descriptor | string | Numéro | Texte de l'état bancaire (max. 22 caractères) |
| tax_code | string | Numéro | Code de la taxe Stripe |
| price.amount | number | Numéro | Prix en unité monétaire la plus petite (en cents) |
| price.currency | string | Numéro | Code de devise ISO 4217 (par exemple "usd", "jmd") |
| price.tax_behavior | string | Numéro | inclusivement, exclusivement ou non |
Clients
Gérer les dossiers clients pour les achats et les abonnements répétés.
| Méthode | Voie | Désignation des marchandises |
|---|---|---|
| POST | /v1/customers | Créer un client |
| GET | /v1/customers | Liste des clients |
| GET | /v1/customers/:id | Obtenir un client |
| PUT | /v1/customers/:id | Mettre à jour un client |
| DELETE | /v1/customers/:id | Supprimer un client |
Créer un client
const customer = await handypay("/customers", {
method: "POST",
body: JSON.stringify({
email: "customer@example.com",
name: "Jane Doe",
}),
});
console.log(customer.id); // "cus_abc123"exemple 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"
}'Organisme de demande
| Champ | Type | Requis | Désignation des marchandises |
|---|---|---|---|
| string | Oui | Courriel du client | |
| name | string | Numéro | Nom du client |
| phone | string | Numéro | Numéro de téléphone du client |
| metadata | object | Numéro | Paires personnalisées de valeurs de clé |
Séances de paiement
Créer des sessions de paiement hébergées pour des paiements ponctuels. Le prix standard de HandyPay s'applique: 4,9% + US$0.40 par transaction sur le forfait gratuit, ou 4,2% + US$0.40 sur Pro. Il n'y a pas de frais supplémentaires sur API ou plate-forme en plus.
| Méthode | Voie | Désignation des marchandises |
|---|---|---|
| POST | /v1/payment-sessions | Créer une session de paiement |
| GET | /v1/payment-sessions/:id | Obtenir l'état de la session |
| GET | /v1/test-payments | Liste des paiements de test (hp_test_ seulement) |
Créer une session de paiement (avec le prix existant)
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;exemple 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"
}'Créer une session de paiement (montant personnalisé)
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",
}),
});exemple 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"
}'Organisme de demande
| Champ | Type | Requis | Désignation des marchandises |
|---|---|---|---|
| line_items | array | Oui | Au moins une ligne |
| success_url | string | Oui | Rediriger URL après le paiement réussi |
| cancel_url | string | Oui | Rediriger URL si le client annule |
| customer_id | string | Numéro | Client existant ID |
| customer_email | string | Numéro | Préremplir l’e-mail (si aucun customer_id) |
| pass_fees_to_customer | boolean | Numéro | Ajouter les frais de traitement et de service au total du client |
| metadata | object | Numéro | Paires personnalisées de valeurs de clé |
| collect_shipping_address | boolean | Numéro | Recueillir une adresse d'expédition |
| billing_address_collection | string | Numéro | auto ou obligatoire |
| shipping_countries | string[] | Numéro | Pays de destination autorisés ISO-2 |
| shipping_options | array | Numéro | Étiquettes et montants d'expédition dans la plus petite unité monétaire |
Champs des rubriques
| Champ | Type | Requis | Désignation des marchandises |
|---|---|---|---|
| price_id | string | Numéro | Stripe Price ID existant |
| amount | number | Numéro | Montant personnalisé en cents |
| currency | string | Numéro | Montant requis |
| name | string | Numéro | Montant requis |
| quantity | number | Oui | Quantité |
Fournir les deux price_id ou amount+currency+name par article de ligne.
{CHECKOUT_SESSION_ID} dans le succès URL. Interrogez ID avec le même marchand et le même mode clé live/test qui l'a créé. La session reste interrogeable après l'achèvement, mais votre webhook signé devrait toujours conduire l'exécution de facture.Paiements intégrés
Créez un PaymentIntent depuis votre serveur lorsque vous souhaitez afficher Stripe Elements sur votre propre page de paiement. Votre clé API HandyPay reste côté serveur ; envoyez au navigateur uniquement la clé publiable renvoyée, l’ID du compte connecté et le secret client à courte durée de vie.
| Méthode | Voie | Désignation des marchandises |
|---|---|---|
| POST | /v1/payment-intents | Créer un PaymentIntent intégré |
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,
};| Champ | Type | Requis | Désignation des marchandises |
|---|---|---|---|
| amount | number | Oui | Entier positif dans la plus petite unité monétaire |
| currency | string | Oui | ISO 4217 code de devise à trois lettres |
| description | string | Numéro | Description du paiement |
| customer_email | string | Numéro | Courriel du client pour les reçus et le rapprochement |
| pass_fees_to_customer | boolean | Numéro | Augmentation du montant pour couvrir les frais |
| metadata | object | Numéro | Identifiants de votre commande ou facture |
Montant des restitutions
Remboursement d'un paiement appartenant au marchand authentifié. HandyPay vérifie la propriété du paiement et le solde disponible avant de créer l'inversion. Omettre amount pour un remboursement complet.
| Méthode | Voie | Désignation des marchandises |
|---|---|---|
| POST | /v1/refunds | Créer un remboursement intégral ou partiel |
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);| Champ | Type | Requis | Désignation des marchandises |
|---|---|---|---|
| session_id | string | Conditionnel | ID de Checkout Session (cs_...). Utilisez ce champ ou payment_intent |
| payment_intent | string | Conditionnel | ID du PaymentIntent (pi_...). Utilisez ce champ ou session_id |
| amount | number | Numéro | Montant du remboursement partiel dans la plus petite unité monétaire |
| reason | string | Numéro | duplicate, fraudulent ou requested_by_customer |
Un paiement contesté, entièrement remboursé, entrecroisé ou insuffisant est rejeté sans créer de remboursement.
Différends
Examiner les frais de remise pour le commerçant connecté et fournir des preuves avant la date d'échéance.
| Méthode | Voie | Désignation des marchandises |
|---|---|---|
| GET | /v1/disputes | Liste des litiges |
| GET | /v1/disputes/:id | Obtenez un différend et son état de preuve |
| POST | /v1/disputes/:id/evidence | Mettre à jour ou présenter des éléments de preuve |
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,
}),
});| Champ | Type | Requis | Désignation des marchandises |
|---|---|---|---|
| evidence | object | Numéro | Stripe champs de preuve de litige comme des valeurs de chaîne |
| submit | boolean | Numéro | Ne le confirmer que lorsque les preuves sont complètes et prêtes à être examinées |
Abonnements
Créez des produits récurrents et gérez les abonnements.
Produits d'abonnement
| Méthode | Voie | Désignation des marchandises |
|---|---|---|
| POST | /v1/subscription-products | Créer un produit d'abonnement |
| GET | /v1/subscription-products | Liste des produits d'abonnement |
Sessions d'abonnement et gestion
| Méthode | Voie | Désignation des marchandises |
|---|---|---|
| POST | /v1/subscription-sessions | Créer un abonnement à la caisse |
| GET | /v1/subscriptions | Liste des abonnements actifs |
| PATCH | /v1/subscriptions/:id/quantity | Changer les sièges avec une proration explicite |
| POST | /v1/subscriptions/:id/cancel | Annuler à la fin de la période de facturation |
Intervalles de facturation pris en charge
Créer un produit d'abonnement
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 sessionsexemple 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
}'Organisme de demande
| Champ | Type | Requis | Désignation des marchandises |
|---|---|---|---|
| name | string | Oui | Nom du produit |
| description | string | Numéro | Description du produit |
| amount | number | Oui | Prix en unité monétaire la plus petite |
| currency | string | Oui | Code de devise ISO 4217 |
| interval | string | Oui | Un des intervalles supportés |
| trial_period_days | number | Numéro | Durée de l'essai libre en jours |
| metadata | object | Numéro | Paires personnalisées de valeurs de clé |
Créer une caisse avec plusieurs sièges
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",
}),
});Changer de siège sur un abonnement actif
La quantité doit être un entier de 1 à 1 000. Choisissez comment le rajustement de facturation est géré au lieu de compter sur un défaut implicite.
const updated = await handypay("/subscriptions/sub_123/quantity", {
method: "PATCH",
body: JSON.stringify({
quantity: 8,
proration_behavior: "create_prorations",
}),
});| Champ | Type | Requis | Désignation des marchandises |
|---|---|---|---|
| quantity | number | Oui | Nombre de nouveaux sièges de 1 à 1 000 |
| proration_behavior | string | Numéro | create_prorations, always_invoice ou none |
| item_id | string | Numéro | Poste d'abonnement spécifique lorsqu'un abonnement comporte plusieurs produits |
Webhooks
Recevez des notifications d'événements en temps réel via HTTP POST sur vos paramètres.
| Méthode | Voie | Désignation des marchandises |
|---|---|---|
| POST | /v1/webhook-endpoints | Enregistrer un paramètre |
| GET | /v1/webhook-endpoints | Liste des paramètres |
| DELETE | /v1/webhook-endpoints/:id | Désactiver un paramètre |
- Les points d'arrivée doivent utiliser HTTPS.
- Les paramètres Webhook enregistrés avec un
hp_test_clé recevoir des événements de test seulement. Les destinations en direct et les destinations de test sont stockées séparément. - Chaque paramètre actif souscrit à un événement reçoit une livraison indépendante signée avec son propre secret. Ne réutilisez pas le secret d'un point de départ pour un autre.
- Retourner une réponse 2xx dans les 10 secondes. Stocker chaque événement
idavant les effets secondaires, les livraisons en double sont donc sûres. - Après 10 échecs consécutifs, un paramètre est automatiquement désactivé.
Types d'événements pris en charge
- 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 l’événement identifie le PaymentIntent dans data.id; utilisez vos métadonnées (par exemple order_id) pour la réconcilier. Utilisation checkout.session.expired pour l'expiration de la session et les événements d'async pour les méthodes de paiement différé.Vérification des signatures de webhook
Chaque livraison comprend une X-HandyPay-Signature en-tête dans le format sha256={hex}- Oui. Vérifiez en calculant HMAC-SHA256 du corps de requête brute en utilisant le secret de signature de votre point de départ:
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);
}Gestionnaire de route API 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 });
}Gestionnaire 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;Format de la charge utile Webhook
{
"id": "evt_abc123",
"type": "payment_intent.succeeded",
"created": 1706745600,
"data": { ... }
}Compte
Lisez les frais et la disponibilité de paiement du marchand connecté plus le compte bancaire de paiement par défaut. Les numéros de banque et de routage sont masqués; seuls les quatre derniers chiffres sont retournés.
| Méthode | Voie | Désignation des marchandises |
|---|---|---|
| GET | /v1/account | Obtenez des détails de compte connecté et de paiement masqué |
const account = await handypay("/account");
console.log({
chargesEnabled: account.chargesEnabled,
payoutsEnabled: account.payoutsEnabled,
bank: account.bankAccount?.bankName,
last4: account.bankAccount?.last4,
});Codes d'erreur
| Code | HTTP | Désignation des marchandises |
|---|---|---|
| unauthorized | 401 | Clé API manquante ou invalide |
| key_revoked | 401 | API clé a été révoqué |
| key_expired | 401 | La clé API a expiré |
| rate_limit_exceeded | 429 | Trop de demandes |
| validation_error | 400 | Demande de validation du corps échoué |
| invalid_url | 400 | success_url ou cancel_url non valide |
| invalid_interval | 400 | Intervalle de facturation non pris en charge |
| product_not_found | 404 | Produit inexistant |
| customer_not_found | 404 | Le client n'existe pas |
| session_not_found | 404 | La session de vérification n'existe pas |
| payment_not_found | 404 | Le paiement est manquant ou n'est pas la propriété de ce marchand |
| subscription_not_found | 404 | L'abonnement n'existe pas |
| refund_not_allowed | 400 | Le paiement est contesté et ne peut être remboursé |
| already_refunded | 400 | Le paiement est déjà intégralement remboursé |
| refund_amount_too_large | 400 | Montant supérieur au solde remboursable restant |
| insufficient_balance | 400 | Le solde disponible ne peut pas couvrir la restitution |
| balance_verification_failed | 400 | La propriété ou la disponibilité du solde n'a pas pu être vérifiée |
| endpoint_not_found | 404 | Paramètres de API inconnus |
| stripe_error | 502 | Stripe API a retourné une erreur |
| internal_error | 500 | Erreur inattendue du serveur |
| payload_too_large | 413 | Organisme de demande supérieur à 1 Mo |
| prohibited_content | 400 | Le contenu viole la politique d'utilisation acceptable |
| webhook_url_must_be_https | 400 | Webhook URL doit utiliser HTTPS |
| key_creation_rate_exceeded | 429 | Trop de clés créées dans la fenêtre de temps |
| max_keys_reached | 400 | Les clés API actives maximales atteintes (25) |
Sécurité
- Gardez
hp_live_ethp_test_les clés sur votre serveur. Ne les placez jamais dans le navigateur JavaScript, applications mobiles, URL, journaux ou contrôle de source. - Toutes les réponses comprennent :
X-Content-Type-Options: nosniff,X-Frame-Options: DENYetStrict-Transport-SecurityEn-têtes. - Requête de la limite de corps : 1 Mio, y compris les demandes en streaming ou en morceaux.
- Les paramètres Webhook doivent utiliser HTTPS.
- Vérifier les signatures du webhook contre les octets bruts exacts et comparer les digests en temps constant.
- Les noms et descriptions de produits sont examinés à l'aide d'une liste de blocs de contenu interdite.
- 5+ violations de contenu dans 24 heures suspendra vos clés API.
Limites des clés API
- Max 3 clés créées par fenêtre de 10 minutes.
- Max 10 clés créées par fenêtre d'une heure.
- Max 25 clés actives par marchand.