API PlanetPay — v1
L'API PlanetPay permet à votre site ou application, depuis votre solde PlanetPay :
-
d'encaisser des paiements (Mobile Money dans 20 pays d'Afrique et carte bancaire internationale) ;
-
d'envoyer de l'argent vers des comptes Mobile Money ;
-
de revendre des services numériques (licences, locations d'outils, crédits, IMEI) ;
-
d'acheter des numéros virtuels pour recevoir un code SMS ;
-
de créer et gérer des cartes virtuelles Visa / Mastercard.
-
URL de base :
https://planetpay.site -
Format : JSON (
Content-Type: application/json) -
Devise du compte : USD. Les montants que vous envoyez sont en USD ; PlanetPay convertit dans la devise locale au taux du moment.
Sommaire
- Démarrage rapide
- Authentification
- Mode test
- Points d'accès
- Services numériques (licences, location d'outils, crédits)
- Numéros virtuels (codes SMS)
- Cartes virtuelles Visa
- Paiement intégré (iframe)
- Plugin SMM panel
- Webhooks
- API partenaires (plateformes connectées)
- Erreurs
- Exemples complets
Démarrage rapide
- Connectez-vous à votre espace PlanetPay, menu API Credentials, et créez une clé. Vous obtenez une clé API (publique) et une clé secrète (à garder sur votre serveur, jamais dans une application mobile ou du JavaScript côté navigateur).
- Les nouvelles clés sont en mode test. Testez votre intégration, puis basculez la clé en production (vérification d'identité requise).
- Encaissez :
POST /v1/rechargerenvoie unpayment_urloù vous redirigez votre client. Votre solde est crédité dès la confirmation, et nous appelons votrecallback_url.
Authentification
Chaque requête porte trois en-têtes :
| En-tête | Valeur |
|---|---|
X-Api-Key |
votre clé API |
X-Timestamp |
l'heure actuelle en secondes Unix (tolérance ± 5 minutes) |
X-Signature |
HMAC-SHA256(clé_secrète, chaîne_à_signer) en hexadécimal minuscule |
La chaîne à signer est :
{X-Timestamp}.{MÉTHODE}.{chemin}.{corps_brut}
MÉTHODEen majuscules (GET,POST)cheminsans le domaine ni les paramètres, par exemple/v1/rechargecorps_brut: le JSON exact envoyé, octet pour octet (chaîne vide pour unGET)
Exemple pour GET /v1/balance à 1792180000 : 1792180000.GET./v1/balance.
La clé secrète ne circule jamais sur le réseau, et une requête interceptée ne peut pas être rejouée après 5 minutes.
Limite : 120 requêtes par minute et par clé (réponse 429 au-delà).
Mode test
Une clé en mode test simule tout, sans aucun mouvement d'argent :
GET /v1/balancerenvoie toujours 1 000 USD ;POST /v1/rechargerenvoie unpayment_urlvers un simulateur où l'on choisit « paiement réussi » ou « échec » (même intégration qu'en production, iframe comprise) ;POST /v1/transferestcompleted, sauf si le numéro se termine par 4444 : il est alorsfailed(pour tester vos cas d'erreur) ;- les webhooks sont envoyés comme en production ;
- les identifiants commencent par
rch_test_/trf_test_. POST /v1/services/{id}/ordersest livré tout de suite avec un résultat facticeTEST-…;POST /v1/cardsrenvoie une carte factice (last4=4242), etfund/withdrawsontcompletedsans débit.POST /v1/numbersrenvoie un numéro factice (+10000000000) qui a déjà « reçu » le code123456(status: completed), sans débit ; les identifiants commencent parnum_test_.
Points d'accès
GET /v1/account
Informations du compte.
{
"object": "account",
"id": "acct_33",
"name": "Awa Koné",
"email": "awa@example.com",
"kyc_verified": true,
"currency": "USD",
"mode": "live"
}
GET /v1/balance
{
"object": "balance",
"mode": "live",
"available": [
{ "currency": "USD", "amount": 70 },
{ "currency": "EUR", "amount": 0 }
]
}
GET /v1/operators — réseaux pour les transferts
Réseaux Mobile Money vers lesquels vous pouvez envoyer de l'argent avec /v1/transfer. Cette liste ne concerne pas les encaissements : /v1/recharge ne demande aucun opérateur.
{
"object": "list",
"data": [
{ "country": "CI", "country_name": "Côte d'Ivoire", "operator": "ORANGE", "name": "Orange Money",
"currency": "XOF", "dial_code": "225", "transfer": true, "recharge": true }
]
}
POST /v1/recharge — encaisser un paiement
Crée une demande de paiement. Redirigez votre client vers payment_url (ou ouvrez-la en iframe) : il paie en Mobile Money ou par carte sur la page sécurisée PlanetPay. Vous n'avez aucun opérateur à choisir ni à transmettre : il suffit d'indiquer la devise, la page propose automatiquement les moyens de paiement du pays du client. Votre solde USD est crédité de amount dès la confirmation.
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
amount |
nombre | oui | montant à créditer, en USD. Pas de limite de montant |
currency |
texte | oui | devise de paiement du client : XOF, XAF, GHS, GNF, KES, NGN, UGX, CDF, RWF, SLE, TZS, ZMW (Mobile Money) ou USD (carte internationale) |
reference |
texte | oui | votre référence unique (lettres, chiffres, -_., 100 max.) |
callback_url |
URL https | non | adresse appelée à la confirmation (voir Webhooks) |
return_url |
URL https | non | page où revient le client après paiement |
Réponse 201 :
{
"id": "rch_vcxzimvbpzaau6oxipm6",
"object": "recharge",
"reference": "ORDER-1001",
"amount": 20,
"account_currency": "USD",
"amount_due": 11698,
"currency": "XOF",
"mode": "live",
"status": "pending",
"payment_url": "https://planetpay.site/pay/checkout/PA261016…",
"expires_at": "2026-10-17T20:00:00+00:00",
"created_at": "2026-10-16T20:00:00+00:00"
}
amount_due est ce que paie votre client dans sa devise (frais de la méthode compris). Le lien expire après 24 h.
POST /v1/transfer — envoyer de l'argent en Mobile Money
Débite votre solde et envoie l'argent sur un compte Mobile Money.
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
amount |
nombre | oui | montant en USD reçu (converti en devise locale) |
country |
texte | oui | code pays ISO à 2 lettres (CI, SN, CM…) |
operator |
texte | oui | code opérateur de /v1/operators (ORANGE, MTN, WAVE…) |
phone |
texte | oui | numéro du bénéficiaire, format local ou international |
reference |
texte | oui | votre référence unique |
callback_url |
URL https | non | adresse appelée à la confirmation |
Réponse 201 :
{
"id": "trf_k2j5…",
"object": "transfer",
"reference": "PAYOUT-77",
"amount": 10,
"fee": 0.8,
"total_debited": 10.8,
"account_currency": "USD",
"receive_amount": 5848,
"receive_currency": "XOF",
"country": "SN",
"operator": "WAVE",
"phone": "221770000000",
"mode": "live",
"status": "pending",
"created_at": "2026-10-16T20:05:00+00:00"
}
Si l'envoi échoue, le montant débité est remboursé automatiquement sur votre solde et le statut passe à failed.
GET /v1/transactions/{id}
Renvoie l'opération (rch_…, trf_…, ord_…, num_…, crd_…, cfd_… ou cwd_…) avec son statut à jour.
Statuts
| Statut | Signification |
|---|---|
pending |
en attente de confirmation |
completed |
confirmé : solde crédité (recharge) ou argent reçu (transfert) |
failed |
échec ; pour un transfert, votre solde a été remboursé |
Idempotence
La reference est unique par type d'opération et par mode. Renvoyer une requête avec la même référence ne crée pas de doublon : vous recevez l'opération existante (code 200 au lieu de 201). En cas de doute (coupure réseau), renvoyez simplement la même requête.
Services numériques
Revendez sur votre site les licences et activations de logiciels GSM, les locations d'outils à l'heure (UnlockTool, Chimera, TSM, AMT…), les crédits et les services IMEI / numéro de série du catalogue PlanetPay.
Fonctionnement
GET /v1/services: vous lisez le catalogue (prix en USD, déjà définitifs).POST /v1/services/{id}/orders: vous commandez pour votre client ; le prix est débité de votre solde USD.- Le résultat (code, identifiants, activation) arrive dans la réponse, ou plus tard par webhook.
- Si le fournisseur refuse ou échoue, votre solde est remboursé automatiquement (
refunded: true).
GET /v1/services — catalogue
| Paramètre | Description |
|---|---|
category |
rental (location d'outils), licence (licences et crédits), imei (services IMEI / SN) |
q |
recherche dans le nom et l'outil |
page, limit |
pagination (limit : 50 par défaut, 100 max.) |
{
"object": "list", "total": 28, "page": 1, "limit": 50,
"data": [{
"id": 130, "object": "service", "name": "AMT - Android Multi Tool Rent [2 Hours]", "category": "rental",
"tool": "Digital Tool", "price": 0.34, "currency": "USD", "delivery_time": "Instant", "image": null,
"requires": [{ "name": "identifier", "label": "Main identifier (IMEI, serial or account)", "required": true }],
"quantity": null, "info": null
}]
}
| Champ | Description |
|---|---|
price |
prix unitaire débité, en USD |
requires |
champs à envoyer à la commande : identifier toujours, les autres dans fields |
quantity |
{ "min": 1, "max": 100 } pour les services vendus à la quantité (crédits), sinon null |
delivery_time |
délai indicatif annoncé par le fournisseur |
GET /v1/services/{id} — un service
Même format qu'un élément de la liste. 404 si le service n'existe pas ou n'est plus vendu.
POST /v1/services/{id}/orders — commander
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
identifier |
texte (120 max.) | oui | identifiant principal : nom d'utilisateur de l'outil, IMEI, numéro de série… |
fields |
objet | selon requires |
autres champs, par exemple {"email": "client@mail.com"} |
quantity |
entier | non | services vendus à la quantité (borné à min / max) |
reference |
texte | oui | votre référence unique (idempotence) |
callback_url |
URL https | non | appelée à la livraison ou au remboursement |
Réponse 201 :
{
"id": "ord_x8k2…", "object": "service_order", "reference": "RENT-1001",
"service": { "id": 130, "name": "AMT - Android Multi Tool Rent [2 Hours]" },
"quantity": 1, "amount": 0.34, "currency": "USD", "mode": "live",
"status": "completed", "result": "User: … Password: …",
"created_at": "2026-10-24T18:26:36+00:00", "completed_at": "2026-10-24T18:26:36+00:00"
}
status |
Signification |
|---|---|
pending |
commande transmise au fournisseur ; le résultat arrive par le webhook service_order.completed |
completed |
livrée : le résultat est dans result |
failed |
refusée ou échouée : error donne la raison, refunded vaut true (solde recrédité) |
Suivi : GET /v1/transactions/{id} avec l'identifiant ord_….
Numéros virtuels
Achetez un numéro de téléphone temporaire pour recevoir un code de vérification par SMS (WhatsApp, Telegram, Google, Facebook, TikTok…), dans le pays de votre choix. Le prix est débité de votre solde USD au moment où le numéro est attribué.
Si aucun SMS n'arrive avant l'expiration du numéro (environ 20 minutes), ou si vous annulez, la commande est remboursée automatiquement.
Fonctionnement
GET /v1/numbers/services: la liste des services (applications).GET /v1/numbers/services/{service}/countries: les pays disponibles pour ce service, avec prix et stock.POST /v1/numbers: vous achetez un numéro et le donnez à votre client.- Le code arrive par le webhook
number.completed, ou en interrogeantGET /v1/numbers/{id}toutes les 5 à 10 secondes.
GET /v1/numbers/services — services
{
"object": "list", "total": 2,
"data": [
{ "service": "telegram", "name": "Telegram" },
{ "service": "whatsapp", "name": "WhatsApp" }
]
}
Utilisez la valeur service dans les appels suivants.
GET /v1/numbers/services/{service}/countries — pays et prix
{
"object": "list", "service": "whatsapp",
"data": [
{ "country": "usa", "name": "États-Unis", "price": 0.31, "currency": "USD", "stock": 340 },
{ "country": "france", "name": "France", "price": 0.61, "currency": "USD", "stock": 12 }
]
}
Les pays sont triés du moins cher au plus cher. price est le prix final débité ; stock le nombre de numéros disponibles au moment de la lecture. Les prix et le stock bougent : relisez-les juste avant d'acheter.
POST /v1/numbers — acheter un numéro
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
service |
texte | oui | valeur service de /v1/numbers/services (par exemple whatsapp) |
country |
texte | oui | valeur country de /v1/numbers/services/{service}/countries (par exemple france) |
reference |
texte | oui | votre référence unique (idempotence) |
callback_url |
URL https | non | appelée à la réception du SMS ou au remboursement |
Réponse 201 :
{
"id": "num_hejqeog9olf0ufukhzii", "object": "number", "reference": "OTP-1001", "mode": "live",
"service": "whatsapp", "country": "france", "number": "+33612345671",
"price": 0.61, "currency": "USD", "status": "pending",
"sms": null, "code": null, "refunded": false, "reason": null,
"expires_at": "2026-10-28T07:58:09+00:00", "created_at": "2026-10-28T07:38:09+00:00", "completed_at": null
}
GET /v1/numbers/{id} — état et code reçu
Renvoie le numéro avec son état à jour (PlanetPay interroge le fournisseur à chaque appel, au plus une fois toutes les 5 secondes). Quand le SMS est arrivé :
{ "id": "num_hejqeog9olf0ufukhzii", "status": "completed", "sms": "Your WhatsApp code: 482-913", "code": "482913", "…": "…" }
status |
Signification |
|---|---|
pending |
numéro attribué, en attente du SMS |
completed |
SMS reçu : texte complet dans sms, code extrait dans code |
failed |
aucun SMS avant expires_at, ou annulé : refunded vaut true, reason donne la cause |
GET /v1/transactions/{id} renvoie la même chose.
POST /v1/numbers/{id}/cancel — annuler
Annule un numéro qui n'a pas encore reçu de SMS ; votre solde est recrédité tout de suite (status: failed, refunded: true). Un numéro qui a déjà reçu son SMS ne peut plus être annulé (409 already_completed).
Conseils : achetez le numéro seulement quand votre client est prêt à saisir le code ; un numéro sert à un seul code ; si le service refuse le numéro, annulez et rachetez dans un autre pays.
Cartes virtuelles Visa
Créez des cartes virtuelles Visa ou Mastercard en USD pour vous ou vos clients, rechargez-les et retirez leur solde, depuis votre solde PlanetPay. Les cartes fonctionnent pour les paiements en ligne (abonnements, publicité, e-commerce…).
Conditions (identiques à l'application PlanetPay) : compte vérifié (KYC), profil de titulaire de carte avec numéro de téléphone, forfait carte actif, nombre maximum de cartes de votre forfait. Les frais d'émission, de recharge et de retrait de votre forfait sont débités de votre solde USD et visibles dans votre espace.
Sécurité : l'API ne renvoie jamais le numéro complet ni le CVV, seulement les 4 derniers chiffres (
last4). Le titulaire consulte les données complètes dans son espace PlanetPay, après authentification.
Fonctionnement
POST /v1/cards: création de la carte (frais d'émission débités).POST /v1/cards/{id}/fund: chargement de la carte depuis votre solde.- Paiements avec la carte.
POST /v1/cards/{id}/withdraw: retour du solde restant de la carte vers votre solde.
L'objet carte
{
"id": "card_01JB7Q9X…", "object": "card", "name": "Awa Traore", "brand": "Visa", "last4": "4821",
"expiry": "10/29", "balance": 25.0, "currency": "USD", "status": "enabled", "created_at": "2026-10-24T18:30:00+00:00"
}
| Champ | Description |
|---|---|
id |
identifiant de la carte (card_…), à utiliser dans les autres appels |
name |
nom imprimé sur la carte |
brand |
Visa ou Mastercard |
last4, expiry |
4 derniers chiffres, date d'expiration MM/AA |
balance |
solde de la carte en USD |
status |
enabled = utilisable ; toute autre valeur = carte bloquée ou fermée |
GET /v1/cards — vos cartes
{ "object": "list", "mode": "live", "data": [ { "id": "card_…", "object": "card", "…": "…" } ] }
Avec une clé de test, la liste est vide : les cartes de test n'existent que dans les réponses de POST /v1/cards.
GET /v1/cards/{id} — une carte
La carte, avec son solde, ses 4 derniers chiffres et son expiration lus en direct chez l'émetteur.
POST /v1/cards — créer une carte
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
name_on_card |
texte (4 à 50) | oui | nom imprimé sur la carte |
brand |
visa | mastercard |
non | visa par défaut |
reference |
texte | oui | votre référence unique (idempotence) |
callback_url |
URL https | non | adresse de notification |
Réponse 201 :
{
"id": "crd_p3k9…", "object": "card_create", "reference": "CARD-1001", "mode": "live", "status": "completed",
"amount": null, "created_at": "2026-10-24T18:30:00+00:00",
"card": { "id": "card_01JB7Q9X…", "object": "card", "name": "Awa Traore", "brand": "Visa", "last4": "4821",
"expiry": "10/29", "balance": 0.0, "currency": "USD", "status": "enabled" }
}
POST /v1/cards/{id}/fund — recharger
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
amount |
nombre (USD, 1 à 100 000) | oui | montant chargé sur la carte (les frais de recharge s'ajoutent) |
reference |
texte | oui | votre référence unique |
callback_url |
URL https | non | adresse de notification |
Réponse 201 (cfd_…) avec amount et la carte à jour dans card.
POST /v1/cards/{id}/withdraw — retirer vers votre solde
Mêmes champs que la recharge. Le montant revient sur votre solde USD (les frais de retrait de votre forfait s'appliquent). Réponse 201 (cwd_…).
Erreurs propres aux cartes
| HTTP | code |
Cause |
|---|---|---|
| 402 | insufficient_balance |
solde PlanetPay (ou solde de la carte, pour un retrait) insuffisant |
| 403 | kyc_required |
clé de production sur un compte non vérifié |
| 404 | not_found |
carte inconnue ou appartenant à un autre compte |
| 422 | card_refused |
règle du forfait non respectée (pas de forfait, limite de cartes, profil titulaire ou téléphone manquant, montant…) ; message donne la raison |
| 502 | card_failed |
l'émetteur n'a pas pu créer la carte ; vérifiez votre historique puis réessayez plus tard |
Paiement intégré (iframe)
Votre client paie sans quitter votre site : la fenêtre PlanetPay s'ouvre par-dessus votre page (plein écran sur mobile).
<script src="https://planetpay.site/js/planetpay.js"></script>
<button id="pay">Payer</button>
<script>
document.getElementById('pay').onclick = async function () {
// Votre serveur appelle POST /v1/recharge et renvoie payment_url (ne mettez jamais la clé secrète dans le navigateur)
const { payment_url } = await fetch('/ma-route/creer-paiement', { method: 'POST' }).then(r => r.json());
PlanetPay.open(payment_url, {
onSuccess: function (e) { /* afficher « merci » ; la livraison attend le webhook */ },
onFailure: function (e) { /* proposer de réessayer */ },
onClose: function (e) { }
});
};
</script>
| Option | Rôle |
|---|---|
onSuccess(e) |
le client a payé (e.reference, e.mode) |
onFailure(e) |
le paiement a échoué |
onClose(e) |
la fenêtre a été fermée |
autoClose |
fermer automatiquement après succès (true par défaut, délai autoCloseDelay = 2500 ms) |
closeOnBackdrop |
fermer en cliquant à côté (true par défaut) |
Sans JavaScript, utilisez une simple redirection vers payment_url (avec return_url) ou une balise <iframe src="payment_url" allow="payment *">. La page de paiement envoie alors à votre page des messages postMessage de la forme { "source": "planetpay", "event": "ready" | "success" | "failed" | "close", "reference": "…" } ; vérifiez que event.origin vaut https://planetpay.site.
Important : l'événement du navigateur sert à l'affichage. Ne livrez une commande qu'après le webhook signé ou GET /v1/transactions/{id}.
Plugin SMM panel
Vous avez un SMM panel (PHP + MySQL) ? Vos clients rechargent leur solde par Mobile Money ou carte, dans une fenêtre PlanetPay intégrée au panel, et le solde est crédité automatiquement.
📦 Télécharger le plugin (version 2.1, PHP 7.4+, sans dépendance)
- Déposez le dossier
planetpaydans le dossier public du panel (https://votre-panel.com/planetpay/). - Exécutez
install.sqldans la base du panel. - Copiez
config.sample.phpenconfig.php(base et mot de passe), puis collez vos clés API dansplanetpay/settings.php. Réglez aussi la base, la table et la colonne du solde (souventusers/balance), la clé de session du client connecté. - Sur la page « Ajouter des fonds » :
<?php include $_SERVER['DOCUMENT_ROOT'] . '/planetpay/widget.php'; ?> - Ajoutez la tâche cron de secours toutes les 5 minutes :
php /chemin/planetpay/cron.php(crédite les paiements dont le webhook a été manqué). - Testez avec une clé de test, connecté avec un compte listé dans
test_users(« Simuler un paiement réussi »), puis passez en production. Les paiements de test ne sont jamais crédités aux autres clients.
Le plugin identifie le client par la session du panel, vérifie la signature du webhook et l'opération auprès de l'API, puis crédite une seule fois le montant réellement payé. Le mode d'emploi complet est dans README.md.
Webhooks
Quand une opération est confirmée ou échoue, PlanetPay envoie un POST JSON à votre callback_url :
{
"event": "recharge.completed",
"data": { "id": "rch_…", "object": "recharge", "reference": "ORDER-1001", "status": "completed", "…": "…" }
}
Événements : recharge.completed, recharge.failed, transfer.completed, transfer.failed, service_order.completed, service_order.failed, number.completed (SMS reçu, code dans data.code), number.failed (numéro remboursé).
Vérifiez toujours la signature avant de livrer une commande :
| En-tête | Valeur |
|---|---|
X-PlanetPay-Timestamp |
heure d'envoi (secondes Unix) |
X-PlanetPay-Signature |
HMAC-SHA256(clé_secrète, timestamp + "." + corps_brut) |
Répondez avec un code 2xx. Sinon, nous réessayons 5 fois (après 10 s, 1 min, 5 min, 15 min et 1 h). Seules les adresses https publiques sont appelées.
API partenaires (plateformes connectées)
Cette API sert aux entreprises qui ont déjà une plateforme en production (bourse, investissement, paiement) et qui veulent transmettre leurs transactions réelles à PlanetPay pour qu'elles apparaissent sur le marché (cours, volumes, bougies). Ce n'est pas un dépôt de projets : seules les données vérifiables sont publiées.
Connexion en 4 étapes
- Demande : dans votre espace PlanetPay, menu Connecter ma plateforme (entreprise, site HTTPS, conformité, URL de votre API d'audit).
- Preuve du domaine : enregistrement DNS TXT
planetpay-verify=<jeton>(sur@ou_planetpay), lu via Google DNS-over-HTTPS, ou fichierhttps://<domaine>/.well-known/planetpay-verify.txtcontenant le jeton. Vérifié automatiquement toutes les heures puis chaque jour ; preuve absente 3 fois de suite = connexion suspendue. - Vérification et phase de test : l'équipe PlanetPay contrôle la conformité et ouvre la phase de test (clés
pk_test_…). L'approbation exige un domaine vérifié et une API d'audit qui a répondu au moins une fois. - Production : clés
pk_live_…, transactions réelles, double vérification de chaque transaction.
Le secret d'une clé (psk_…) n'est affiché qu'une seule fois ; il reste chiffré chez nous. Deux clés actives maximum par mode (rotation : créez la nouvelle, basculez, révoquez l'ancienne).
Authentification partenaire
Base : https://planetpay.site/v1/partner (aussi accessible sous /api/v1/partner). HTTPS uniquement, JSON UTF-8, 64 Ko max.
| En-tête | Valeur |
|---|---|
X-Partner-Key (ou X-Partner-Id) |
pk_test_… / pk_live_… |
X-Timestamp (ou X-Partner-Timestamp) |
heure Unix en secondes (±5 min) |
X-Signature (ou X-Partner-Signature) |
hex(HMAC-SHA256(secret, timestamp + "." + MÉTHODE + "." + chemin + "." + corps_brut)) |
chemin = chemin appelé sans domaine ni paramètres (/v1/partner/events). Une signature n'est acceptée qu'une fois (anti-rejeu) : signez à nouveau chaque requête, même pour une nouvelle tentative. Limite : 300 requêtes/min par clé.
Points d'accès
| Méthode | Chemin | Usage |
|---|---|---|
| GET | /v1/partner/health |
état du service, sans authentification |
| GET | /v1/partner/status |
vérification de connexion : partenaire, statut, mode, domaine, mode réel autorisé |
| POST | /v1/partner/events |
envoyer un événement → 202 accepté, 200 doublon, 409 conflit, 422 invalide |
| GET | /v1/partner/events/{event_id} |
statut d'un événement envoyé |
| GET | /v1/partner/events?status=&type=&limit=&before= |
liste (plus récents d'abord, curseur next_before) |
| GET | /v1/partner/assets |
vos actifs connus de PlanetPay : statut (listé ou non), dernier prix vérifié, variation et volume 24 h |
| GET | /v1/partner/transactions/{trade_id} |
une transaction vue par PlanetPay : conservée ou annulée, double vérification, présence dans les cours, commissions |
| GET | /v1/partner/commissions?status=&limit=&before= |
lignes de commission (plus récentes d'abord, curseur next_before) |
| GET | /v1/partner/reports?from=AAAA-MM-JJ&to=AAAA-MM-JJ |
rapport de période (30 jours par défaut, 366 max) : événements, transactions, volume par actif, commissions, relevés, règle tarifaire |
Format d'un événement
{
"event_id": "evt_20261028_000123",
"type": "trade.executed",
"occurred_at": "2026-10-28T09:30:12Z",
"data": {
"trade_id": "TR-88412", "order_id": "OR-5521", "asset_id": "SNTS",
"asset_name": "Sonatel", "asset_type": "shares",
"side": "buy", "quantity": "10", "price": "25500", "currency": "XOF",
"fee": "150", "source_ref": "https://api.ma-plateforme.com/trades/TR-88412"
}
}
| type | obligatoire dans data |
|---|---|
trade.executed |
trade_id, asset_id, side, quantity, price, currency |
trade.partially_executed |
trade_id, order_id, asset_id, side, quantity, price, currency |
order.created, order.accepted |
order_id, asset_id, side, quantity, currency |
order.cancelled, order.rejected |
order_id, asset_id |
transaction.settled, transaction.reversed |
transaction_id, amount, currency (+ trade_id pour annuler une transaction) |
asset.price_updated |
asset_id, price, currency (prix indicatif, n'entre pas dans les bougies) |
Règles : event_id unique chez vous (renvoyé à l'identique = doublon, autre contenu = 409) ; occurred_at ISO 8601 avec fuseau, ni futur ni plus de 30 jours ; décimaux positifs en chaîne (8 décimales max) ; trade_id identifie une exécution (jamais comptée deux fois). Un ordre n'est jamais traité comme une transaction exécutée. Chaque actif est propre à votre plateforme : aucun mélange avec les cours d'autres partenaires.
Double vérification (anti-fraude)
Une transaction reçue n'est pas publiée tout de suite. PlanetPay appelle votre API d'audit et ne retient la transaction que si elle existe chez vous avec le même actif, la même quantité, le même prix et la même devise, au statut settled.
GET https://api.ma-plateforme.com/planetpay/audit/transaction/{id}
X-PlanetPay-Partner: ptn_…
X-PlanetPay-Timestamp: 1761643812
X-PlanetPay-Signature: hex(HMAC-SHA256(secret_de_la_clé, timestamp + ".GET." + chemin_et_paramètres + "."))
200 {"trade_id":"TR-88412","asset_id":"SNTS","quantity":"10","price":"25500","currency":"XOF","status":"settled"}
- Pas encore
settled: nouvel essai après 1, 5, 15 min, 1, 3, 6 et 12 h, puis abandon (non publiée). - 404 trois fois, chiffres différents, ou statut
reversed/cancelled/failed: incohérence, transaction refusée. - 3 incohérences en 24 h : plateforme suspendue automatiquement.
- HTTPS sur votre domaine vérifié uniquement, sans redirection, réponse < 8 s et < 16 Ko.
Webhooks partenaires (notifications de PlanetPay)
Configurez une URL HTTPS sur votre domaine (onglet Webhooks) : PlanetPay vous prévient dès qu'il se passe quelque chose, sans que vous ayez à interroger l'API.
| Événement | Quand |
|---|---|
event.accepted / event.rejected |
un événement envoyé est accepté / rejeté (avec l'erreur) |
trade.verified |
transaction confirmée par la double vérification, publiée |
trade.mismatch / trade.failed |
transaction refusée / jamais confirmée |
partner.status_changed |
statut de votre plateforme modifié |
asset.status_changed |
un de vos actifs listé ou suspendu |
key.revoked |
une clé API révoquée |
webhook.test |
test envoyé depuis la console |
POST https://api.ma-plateforme.com/planetpay/webhooks
X-PlanetPay-Event: trade.verified
X-PlanetPay-Delivery: whd_… (identique à chaque nouvel essai : dédoublonnez dessus)
X-PlanetPay-Timestamp: 1761643815
X-PlanetPay-Signature: hex(HMAC-SHA256(whsec_…, timestamp + "." + corps_brut))
{"id":"whd_…","type":"trade.verified","created_at":"2026-10-28T09:30:15Z","partner_id":"ptn_…","mode":"live",
"data":{"trade_id":"TR-88412","asset_id":"SNTS","quantity":"10","price":"25500","currency":"XOF","attempts":1,"error":null}}
Répondez un code 2xx en moins de 10 s (traitez ensuite en file d'attente). Sinon : nouvel essai après 1, 5, 30 min, 2, 6, 12 et 24 h, puis échec (renvoi possible depuis la console). Le secret whsec_… est distinct de vos clés API et s'affiche une seule fois.
Commissions PlanetPay
PlanetPay prélève une commission sur chaque transaction que vous déclarez, selon votre contrat : pourcentage achat, pourcentage vente et/ou montant fixe par transaction (dans la devise de la transaction). La règle est figée sur la ligne au moment où la transaction arrive : un changement de tarif ne réécrit jamais l'historique, et un événement reçu plusieurs fois ne crée qu'une seule ligne.
| Statut | Signification |
|---|---|
pending |
transaction pas encore confirmée par la double vérification, ou encore dans la fenêtre d'annulation (3 jours par défaut) |
earned |
acquise |
invoiced |
portée sur un relevé mensuel |
paid |
relevé réglé |
cancelled |
transaction annulée (transaction.reversed) ou non confirmée (mismatch, failed) |
Les commissions réelles acquises d'un mois terminé sont regroupées sur un relevé mensuel par devise (PS…). Une transaction annulée après facturation génère une ligne adjustment négative portée sur le relevé suivant. Les commissions du mode test sont une simple simulation, jamais facturées. Détail et export CSV : onglet « Commissions » de votre tableau de bord.
GET /v1/partner/transactions/TR-88412
{"trade_id":"TR-88412","asset_id":"SNTS","side":"buy","quantity":"10","price":"25500","currency":"XOF","status":"valid",
"double_check":{"status":"verified","attempts":1,"verified_at":"2026-10-28T09:31:02Z","error":null},"in_market_data":true,
"commissions":[{"trade_id":"TR-88412","kind":"fee","base":"255000","rate_pct":"0.5","fixed":"10","amount":"1285","currency":"XOF","status":"pending","statement":null}]}
Erreurs partenaires
| HTTP | code |
|---|---|
| 400 | invalid_json, https_required |
| 401 | unauthorized, timestamp_expired, replayed_request |
| 403 | partner_suspended, partner_disconnected, partner_rejected, live_not_allowed, not_in_test_phase |
| 409 | event_conflict |
| 413 | payload_too_large |
| 422 | invalid_event (enregistré comme rejeté ; renvoyez-le corrigé avec le même event_id) |
| 429 | rate_limited |
| 503 | service_unavailable |
Exemple complet (PHP)
// 1. envoyer une transaction exécutée
$body = json_encode($event, JSON_UNESCAPED_SLASHES);
$ts = (string) time();
$sig = hash_hmac('sha256', $ts . '.POST./v1/partner/events.' . $body, getenv('PLANETPAY_PARTNER_SECRET'));
$ch = curl_init('https://planetpay.site/v1/partner/events');
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-Partner-Key: ' . getenv('PLANETPAY_PARTNER_KEY'), "X-Timestamp: $ts", "X-Signature: $sig"]]);
$res = curl_exec($ch);
// 2. votre API d'audit (Laravel)
Route::get('/planetpay/audit/transaction/{id}', function (Request $r, string $id) {
$ts = $r->header('X-PlanetPay-Timestamp');
$ok = abs(time() - (int) $ts) <= 300 && hash_equals(hash_hmac('sha256', $ts . '.GET.' . $r->getRequestUri() . '.', env('PLANETPAY_PARTNER_SECRET')), (string) $r->header('X-PlanetPay-Signature'));
abort_unless($ok, 401);
$t = Trade::where('reference', $id)->firstOrFail();
return ['trade_id' => $t->reference, 'asset_id' => $t->asset_code, 'quantity' => (string) $t->quantity, 'price' => (string) $t->price, 'currency' => $t->currency, 'status' => $t->status];
});
// 3. recevoir nos webhooks
Route::post('/planetpay/webhooks', function (Request $r) {
$ts = $r->header('X-PlanetPay-Timestamp');
abort_unless(abs(time() - (int) $ts) <= 300 && hash_equals(hash_hmac('sha256', $ts . '.' . $r->getContent(), env('PLANETPAY_WEBHOOK_SECRET')), (string) $r->header('X-PlanetPay-Signature')), 401);
ProcessPlanetPayNotification::dispatch($r->header('X-PlanetPay-Delivery'), $r->json()->all()); // dédoublonnez sur l'id
return response()->noContent();
});
Erreurs
Toutes les erreurs ont la même forme :
{ "error": { "code": "insufficient_balance", "message": "Insufficient balance.", "required": 10.8 } }
| HTTP | code |
Cause |
|---|---|---|
| 401 | unauthorized |
en-têtes manquants, clé inconnue ou signature invalide |
| 401 | timestamp_expired |
X-Timestamp décalé de plus de 5 minutes |
| 403 | kyc_required |
clé de production sur un compte non vérifié |
| 403 | account_disabled |
compte suspendu |
| 402 | insufficient_balance |
solde insuffisant (transfert + frais, service, numéro, carte) |
| 404 | not_found |
opération inconnue |
| 409 | wallet_missing |
portefeuille USD absent |
| 422 | validation_error |
champ invalide (fields détaille chaque champ) |
| 422 | currency_not_supported |
devise non disponible |
| 422 | operator_not_supported |
réseau ou pays non disponible pour un transfert |
| 422 | invalid_phone |
numéro invalide |
| 429 | rate_limited |
plus de 120 requêtes par minute |
| 422 | order_refused |
commande de service refusée par le fournisseur (rien n'est débité) |
| 422 | card_refused |
opération de carte refusée (forfait, limite de cartes, montant…) ; le message en donne la raison |
| 502 | card_failed |
la carte n'a pas pu être créée chez l'émetteur |
| 404 | not_found |
service, numéro ou carte inconnu |
| 409 | number_unavailable |
aucun numéro disponible pour ce service et ce pays (rien n'est débité) |
| 409 | already_completed |
le numéro a déjà reçu son SMS : annulation impossible |
| 503 | service_unavailable |
numéros virtuels momentanément indisponibles |
Exemples complets
Les exemples créent une recharge de 20 USD payée en XOF. Remplacez PK et SK par vos clés.
cURL (bash)
PK="votre_cle_api"; SK="votre_cle_secrete"
BODY='{"amount":20,"currency":"XOF","reference":"ORDER-1001","callback_url":"https://votresite.com/planetpay/webhook"}'
TS=$(date +%s)
SIG=$(printf '%s' "$TS.POST./v1/recharge.$BODY" | openssl dgst -sha256 -hmac "$SK" | sed 's/^.* //')
curl -s https://planetpay.site/v1/recharge \
-H "Content-Type: application/json" -H "X-Api-Key: $PK" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \
-d "$BODY"
JavaScript (Node.js 18+)
import crypto from 'node:crypto';
const PK = process.env.PLANETPAY_KEY, SK = process.env.PLANETPAY_SECRET;
async function planetpay(method, path, body) {
const raw = body ? JSON.stringify(body) : '';
const ts = Math.floor(Date.now() / 1000).toString();
const sig = crypto.createHmac('sha256', SK).update(`${ts}.${method}.${path.split('?')[0]}.${raw}`).digest('hex');
const res = await fetch('https://planetpay.site' + path, {
method,
headers: { 'Content-Type': 'application/json', 'X-Api-Key': PK, 'X-Timestamp': ts, 'X-Signature': sig },
body: raw || undefined,
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
return data;
}
const recharge = await planetpay('POST', '/v1/recharge', {
amount: 20, currency: 'XOF', reference: 'ORDER-1001',
callback_url: 'https://votresite.com/planetpay/webhook',
});
console.log(recharge.payment_url);
// Webhook (Express) : vérifier la signature sur le corps BRUT
// app.post('/planetpay/webhook', express.raw({ type: 'application/json' }), (req, res) => {
// const expected = crypto.createHmac('sha256', SK)
// .update(`${req.get('X-PlanetPay-Timestamp')}.${req.body}`).digest('hex');
// if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.get('X-PlanetPay-Signature') || ''))) return res.sendStatus(401);
// const event = JSON.parse(req.body);
// if (event.event === 'recharge.completed') { /* livrer la commande event.data.reference */ }
// res.sendStatus(200);
// });
PHP
<?php
const PK = 'votre_cle_api';
const SK = 'votre_cle_secrete';
function planetpay(string $method, string $path, ?array $body = null): array {
$raw = $body ? json_encode($body) : '';
$ts = (string) time();
$sig = hash_hmac('sha256', "$ts.$method." . strtok($path, '?') . ".$raw", SK);
$ch = curl_init('https://planetpay.site' . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_POSTFIELDS => $raw ?: null,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-Api-Key: ' . PK, "X-Timestamp: $ts", "X-Signature: $sig"],
]);
$data = json_decode(curl_exec($ch), true);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 400) throw new RuntimeException($data['error']['code'] . ': ' . $data['error']['message']);
return $data;
}
$transfer = planetpay('POST', '/v1/transfer', [
'amount' => 10, 'country' => 'SN', 'operator' => 'WAVE', 'phone' => '770000000', 'reference' => 'PAYOUT-77',
]);
echo $transfer['status'];
// Webhook : verify.php
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', ($_SERVER['HTTP_X_PLANETPAY_TIMESTAMP'] ?? '') . '.' . $raw, SK);
if (!hash_equals($expected, $_SERVER['HTTP_X_PLANETPAY_SIGNATURE'] ?? '')) { http_response_code(401); exit; }
$event = json_decode($raw, true);
// ... traiter $event['event'] et $event['data']['reference']
http_response_code(200);
Python 3
import hashlib, hmac, json, time, requests
PK, SK = "votre_cle_api", "votre_cle_secrete"
def planetpay(method, path, body=None):
raw = json.dumps(body, separators=(",", ":")) if body else ""
ts = str(int(time.time()))
sig = hmac.new(SK.encode(), f"{ts}.{method}.{path.split('?')[0]}.{raw}".encode(), hashlib.sha256).hexdigest()
r = requests.request(method, "https://planetpay.site" + path, data=raw or None, timeout=30, headers={
"Content-Type": "application/json", "X-Api-Key": PK, "X-Timestamp": ts, "X-Signature": sig})
data = r.json()
if r.status_code >= 400:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
return data
print(planetpay("GET", "/v1/balance"))
op = planetpay("POST", "/v1/recharge", {"amount": 20, "currency": "XOF", "reference": "ORDER-1001"})
print(op["payment_url"])
# Webhook (Flask) :
# @app.post("/planetpay/webhook")
# def webhook():
# raw = request.get_data()
# expected = hmac.new(SK.encode(), request.headers["X-PlanetPay-Timestamp"].encode() + b"." + raw, hashlib.sha256).hexdigest()
# if not hmac.compare_digest(expected, request.headers.get("X-PlanetPay-Signature", "")):
# return "", 401
# event = json.loads(raw)
# return "", 200
Services numériques, numéros virtuels et cartes Visa : PHP, Python, Node.js, Ruby, cURL
Chaque exemple liste les locations d'outils et en commande une, crée une carte Visa et la recharge de 25 USD, puis achète un numéro virtuel WhatsApp et attend le code SMS. Lancez-les d'abord avec une clé de test : rien n'est débité. Les clés sont lues dans les variables d'environnement PLANETPAY_KEY et PLANETPAY_SECRET.
Le chemin signé ne contient jamais les paramètres d'URL : pour
GET /v1/services?category=rental, on signe/v1/services.
PHP
<?php
define('PK', getenv('PLANETPAY_KEY'));
define('SK', getenv('PLANETPAY_SECRET'));
function planetpay(string $method, string $path, ?array $body = null): array {
$raw = $body ? json_encode($body) : '';
$ts = (string) time();
$sig = hash_hmac('sha256', "$ts.$method." . strtok($path, '?') . ".$raw", SK);
$ch = curl_init('https://planetpay.site' . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_POSTFIELDS => $raw ?: null,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-Api-Key: ' . PK, "X-Timestamp: $ts", "X-Signature: $sig"],
]);
$data = json_decode(curl_exec($ch), true);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 400) throw new RuntimeException($data['error']['code'] . ': ' . $data['error']['message']);
return $data;
}
// 1. Tool rental: list, then order
$rentals = planetpay('GET', '/v1/services?category=rental&q=unlock');
$service = $rentals['data'][0];
echo $service['name'], ' — ', $service['price'], " USD\n";
$order = planetpay('POST', '/v1/services/' . $service['id'] . '/orders', [
'identifier' => 'my-unlocktool-username',
'reference' => 'RENT-' . time(),
'callback_url' => 'https://example.com/planetpay/webhook',
]);
echo $order['status'], ' ', $order['result'] ?? '(pending: wait for the webhook)', "\n";
// 2. Visa card: create, then fund with 25 USD
$card = planetpay('POST', '/v1/cards', ['name_on_card' => 'Awa Traore', 'brand' => 'visa', 'reference' => 'CARD-' . time()]);
$cardId = $card['card']['id'];
$fund = planetpay('POST', "/v1/cards/$cardId/fund", ['amount' => 25, 'reference' => 'FUND-' . time()]);
echo $cardId, ' ', $fund['status'], ' balance ', $fund['card']['balance'], "\n";
// 3. Virtual number: cheapest country for WhatsApp, buy, then wait for the SMS code
$country = planetpay('GET', '/v1/numbers/services/whatsapp/countries')['data'][0]['country'];
$num = planetpay('POST', '/v1/numbers', ['service' => 'whatsapp', 'country' => $country, 'reference' => 'OTP-' . time()]);
echo 'Give this number to your customer: ', $num['number'], "\n";
while ($num['status'] === 'pending') { sleep(5); $num = planetpay('GET', '/v1/numbers/' . $num['id']); }
echo $num['status'] === 'completed' ? 'Code: ' . $num['code'] : 'No SMS, refunded', "\n";
Python 3
import hashlib, hmac, json, os, time, requests
PK, SK = os.environ["PLANETPAY_KEY"], os.environ["PLANETPAY_SECRET"]
def planetpay(method, path, body=None):
raw = json.dumps(body, separators=(",", ":")) if body else ""
ts = str(int(time.time()))
signed = path.split("?")[0]
sig = hmac.new(SK.encode(), f"{ts}.{method}.{signed}.{raw}".encode(), hashlib.sha256).hexdigest()
r = requests.request(method, "https://planetpay.site" + path, data=raw or None, timeout=30, headers={
"Content-Type": "application/json", "X-Api-Key": PK, "X-Timestamp": ts, "X-Signature": sig})
data = r.json()
if r.status_code >= 400:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
return data
# 1. Tool rental: list, then order
service = planetpay("GET", "/v1/services?category=rental&q=unlock")["data"][0]
print(service["name"], "-", service["price"], "USD")
order = planetpay("POST", f"/v1/services/{service['id']}/orders", {
"identifier": "my-unlocktool-username", "reference": f"RENT-{int(time.time())}"})
print(order["status"], order.get("result", "(pending: wait for the webhook)"))
# 2. Visa card: create, then fund with 25 USD
card = planetpay("POST", "/v1/cards", {"name_on_card": "Awa Traore", "brand": "visa", "reference": f"CARD-{int(time.time())}"})
fund = planetpay("POST", f"/v1/cards/{card['card']['id']}/fund", {"amount": 25, "reference": f"FUND-{int(time.time())}"})
print(card["card"]["id"], fund["status"], "balance", fund["card"]["balance"])
# 3. Virtual number: cheapest country for WhatsApp, buy, then wait for the SMS code
country = planetpay("GET", "/v1/numbers/services/whatsapp/countries")["data"][0]["country"]
num = planetpay("POST", "/v1/numbers", {"service": "whatsapp", "country": country, "reference": f"OTP-{int(time.time())}"})
print("Give this number to your customer:", num["number"])
while num["status"] == "pending":
time.sleep(5); num = planetpay("GET", f"/v1/numbers/{num['id']}")
print("Code:", num["code"] if num["status"] == "completed" else "no SMS, refunded")
Node.js 18+
import crypto from 'node:crypto';
const PK = process.env.PLANETPAY_KEY, SK = process.env.PLANETPAY_SECRET;
async function planetpay(method, path, body) {
const raw = body ? JSON.stringify(body) : '';
const ts = Math.floor(Date.now() / 1000).toString();
const signed = path.split('?')[0];
const sig = crypto.createHmac('sha256', SK).update(`${ts}.${method}.${signed}.${raw}`).digest('hex');
const res = await fetch('https://planetpay.site' + path, {
method,
headers: { 'Content-Type': 'application/json', 'X-Api-Key': PK, 'X-Timestamp': ts, 'X-Signature': sig },
body: raw || undefined,
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
return data;
}
// 1. Tool rental: list, then order
const { data: rentals } = await planetpay('GET', '/v1/services?category=rental&q=unlock');
const order = await planetpay('POST', `/v1/services/${rentals[0].id}/orders`, {
identifier: 'my-unlocktool-username', reference: `RENT-${Date.now()}`,
});
console.log(rentals[0].name, order.status, order.result ?? '(pending: wait for the webhook)');
// 2. Visa card: create, then fund with 25 USD
const card = await planetpay('POST', '/v1/cards', { name_on_card: 'Awa Traore', brand: 'visa', reference: `CARD-${Date.now()}` });
const fund = await planetpay('POST', `/v1/cards/${card.card.id}/fund`, { amount: 25, reference: `FUND-${Date.now()}` });
console.log(card.card.id, fund.status, 'balance', fund.card.balance);
// 3. Virtual number: cheapest country for WhatsApp, buy, then wait for the SMS code
const { data: countries } = await planetpay('GET', '/v1/numbers/services/whatsapp/countries');
let num = await planetpay('POST', '/v1/numbers', { service: 'whatsapp', country: countries[0].country, reference: `OTP-${Date.now()}` });
console.log('Give this number to your customer:', num.number);
while (num.status === 'pending') { await new Promise(r => setTimeout(r, 5000)); num = await planetpay('GET', `/v1/numbers/${num.id}`); }
console.log(num.status === 'completed' ? `Code: ${num.code}` : 'No SMS, refunded');
Ruby
require 'json'
require 'net/http'
require 'openssl'
PK = ENV.fetch('PLANETPAY_KEY')
SK = ENV.fetch('PLANETPAY_SECRET')
def planetpay(method, path, body = nil)
raw = body ? JSON.generate(body) : ''
ts = Time.now.to_i.to_s
sig = OpenSSL::HMAC.hexdigest('SHA256', SK, "#{ts}.#{method}.#{path.split('?').first}.#{raw}")
uri = URI("https://planetpay.site#{path}")
req = Net::HTTPGenericRequest.new(method, !raw.empty?, true, uri.request_uri,
'Content-Type' => 'application/json', 'X-Api-Key' => PK, 'X-Timestamp' => ts, 'X-Signature' => sig)
req.body = raw unless raw.empty?
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)
raise "#{data['error']['code']}: #{data['error']['message']}" if res.code.to_i >= 400
data
end
# 1. Tool rental: list, then order
service = planetpay('GET', '/v1/services?category=rental&q=unlock')['data'].first
order = planetpay('POST', "/v1/services/#{service['id']}/orders",
{ identifier: 'my-unlocktool-username', reference: "RENT-#{Time.now.to_i}" })
puts "#{service['name']} #{order['status']} #{order['result'] || '(pending: wait for the webhook)'}"
# 2. Visa card: create, then fund with 25 USD
card = planetpay('POST', '/v1/cards', { name_on_card: 'Awa Traore', brand: 'visa', reference: "CARD-#{Time.now.to_i}" })
fund = planetpay('POST', "/v1/cards/#{card['card']['id']}/fund", { amount: 25, reference: "FUND-#{Time.now.to_i}" })
puts "#{card['card']['id']} #{fund['status']} balance #{fund['card']['balance']}"
# 3. Virtual number: cheapest country for WhatsApp, buy, then wait for the SMS code
country = planetpay('GET', '/v1/numbers/services/whatsapp/countries')['data'][0]['country']
num = planetpay('POST', '/v1/numbers', { service: 'whatsapp', country: country, reference: "OTP-#{Time.now.to_i}" })
puts "Give this number to your customer: #{num['number']}"
while num['status'] == 'pending'
sleep 5
num = planetpay('GET', "/v1/numbers/#{num['id']}")
end
puts num['status'] == 'completed' ? "Code: #{num['code']}" : 'No SMS, refunded'
cURL (bash)
PK="$PLANETPAY_KEY"; SK="$PLANETPAY_SECRET"
pp() { # pp METHOD PATH [JSON]
local ts sig; ts=$(date +%s)
sig=$(printf '%s' "$ts.$1.${2%%\?*}.$3" | openssl dgst -sha256 -hmac "$SK" | sed 's/^.* //')
curl -s "https://planetpay.site$2" -X "$1" -H "Content-Type: application/json" \
-H "X-Api-Key: $PK" -H "X-Timestamp: $ts" -H "X-Signature: $sig" ${3:+-d "$3"}
}
pp GET "/v1/services?category=rental&limit=5"
pp POST /v1/services/130/orders '{"identifier":"my-unlocktool-username","reference":"RENT-1001"}'
pp POST /v1/cards '{"name_on_card":"Awa Traore","brand":"visa","reference":"CARD-1001"}'
pp GET /v1/numbers/services/whatsapp/countries
pp POST /v1/numbers '{"service":"whatsapp","country":"france","reference":"OTP-1001"}'
pp GET /v1/numbers/num_xxxxxxxxxxxxxxxxxxxx # until "status":"completed", then read "code"
Questions techniques : écrivez au support depuis votre espace PlanetPay, en indiquant l'identifiant de l'opération (rch_…, trf_…, ord_…, num_…, crd_……).
