API de PlanetPay — v1
La API de PlanetPay permite a su sitio web o aplicación, desde su saldo PlanetPay:
-
cobrar pagos (Mobile Money en 20 países de África y tarjetas internacionales);
-
enviar dinero a cuentas de Mobile Money;
-
revender servicios digitales (licencias, alquiler de herramientas, créditos, IMEI);
-
comprar números virtuales para recibir un código SMS;
-
crear y gestionar tarjetas virtuales Visa / Mastercard.
-
URL base:
https://planetpay.site -
Formato: JSON (
Content-Type: application/json) -
Moneda de la cuenta: USD. Los importes que envía están en USD; PlanetPay los convierte a la moneda local al tipo de cambio vigente.
Índice
- Inicio rápido
- Autenticación
- Modo de prueba
- Endpoints
- Servicios digitales (licencias, alquiler de herramientas, créditos)
- Números virtuales (códigos SMS)
- Tarjetas virtuales Visa
- Pago integrado (iframe)
- Plugin para SMM panel
- Webhooks
- API de socios (plataformas conectadas)
- Errores
- Ejemplos completos
Inicio rápido
- Inicie sesión en su cuenta PlanetPay, abra API Credentials y cree una clave. Obtendrá una clave API (pública) y una clave secreta (guárdela en su servidor, nunca en una aplicación móvil ni en JavaScript del navegador).
- Las claves nuevas están en modo de prueba. Pruebe su integración y luego pase la clave a producción (se requiere verificación de identidad).
- Cobre:
POST /v1/rechargedevuelve unapayment_urla la que redirige a su cliente. Su saldo se acredita al confirmarse el pago y llamamos a sucallback_url.
Autenticación
Cada solicitud lleva tres cabeceras:
| Cabecera | Valor |
|---|---|
X-Api-Key |
su clave API |
X-Timestamp |
la hora actual en segundos Unix (tolerancia ± 5 minutos) |
X-Signature |
HMAC-SHA256(clave_secreta, cadena_a_firmar) en hexadecimal minúscula |
La cadena a firmar es:
{X-Timestamp}.{MÉTODO}.{ruta}.{cuerpo_bruto}
MÉTODOen mayúsculas (GET,POST)rutasin dominio ni parámetros, por ejemplo/v1/rechargecuerpo_bruto: el JSON exacto enviado, byte a byte (cadena vacía para unGET)
Ejemplo para GET /v1/balance a las 1792180000: 1792180000.GET./v1/balance.
La clave secreta nunca viaja por la red y una solicitud interceptada no puede repetirse después de 5 minutos.
Límite: 120 solicitudes por minuto y por clave (429 si se supera).
Modo de prueba
Una clave de prueba lo simula todo, sin ningún movimiento de dinero:
GET /v1/balancedevuelve siempre 1 000 USD;POST /v1/rechargedevuelve unapayment_urlhacia un simulador donde se elige «pago correcto» o «fallido» (misma integración que en producción, iframe incluido);POST /v1/transferquedacompleted, salvo si el número termina en 4444: entonces esfailed(para probar sus casos de error);- los webhooks se envían como en producción;
- los identificadores empiezan por
rch_test_/trf_test_. POST /v1/services/{id}/ordersse entrega al instante con un resultado ficticioTEST-…;POST /v1/cardsdevuelve una tarjeta ficticia (last4=4242), yfund/withdrawquedancompletedsin ningún cargo.POST /v1/numbersdevuelve un número ficticio (+10000000000) que ya «recibió» el código123456(status: completed), sin ningún cargo; los identificadores empiezan pornum_test_.
Endpoints
GET /v1/account
{ "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 — redes para transferencias
Redes de Mobile Money a las que puede enviar dinero con /v1/transfer. Esta lista no se usa para los cobros: /v1/recharge no pide ningún operador.
{ "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 — cobrar un pago
Crea una solicitud de pago. Redirija a su cliente a payment_url (o ábrala en un iframe): paga con Mobile Money o con tarjeta en la página segura de PlanetPay. No tiene que elegir ni enviar ningún operador: basta con indicar la moneda, la página ofrece automáticamente los medios de pago del país del cliente. Su saldo en USD se acredita con amount al confirmarse.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
amount |
número | sí | importe a acreditar, en USD. Sin límite de importe |
currency |
texto | sí | moneda de pago del cliente: XOF, XAF, GHS, GNF, KES, NGN, UGX, CDF, RWF, SLE, TZS, ZMW (Mobile Money) o USD (tarjeta internacional) |
reference |
texto | sí | su referencia única (letras, cifras, -_., máx. 100) |
callback_url |
URL https | no | se llama al confirmarse (ver Webhooks) |
return_url |
URL https | no | página a la que vuelve el cliente tras pagar |
Respuesta 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 es lo que paga su cliente en su moneda (comisiones del método incluidas). El enlace caduca a las 24 h.
POST /v1/transfer — enviar Mobile Money
Debita su saldo y envía el dinero a una cuenta de Mobile Money.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
amount |
número | sí | importe en USD (convertido a la moneda local) |
country |
texto | sí | código de país ISO de 2 letras (CI, SN, CM…) |
operator |
texto | sí | código de operador de /v1/operators (ORANGE, MTN, WAVE…) |
phone |
texto | sí | número del beneficiario, formato local o internacional |
reference |
texto | sí | su referencia única |
callback_url |
URL https | no | se llama al confirmarse |
Respuesta 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 el envío falla, el importe debitado se reembolsa automáticamente y el estado pasa a failed.
GET /v1/transactions/{id}
Devuelve la operación (rch_…, trf_…, ord_…, num_…, crd_…, cfd_… o cwd_…) con su estado actualizado.
Estados
| Estado | Significado |
|---|---|
pending |
pendiente de confirmación |
completed |
confirmado: saldo acreditado (recarga) o dinero recibido (transferencia) |
failed |
fallido; en una transferencia, su saldo ha sido reembolsado |
Idempotencia
reference es única por tipo de operación y modo. Reenviar una solicitud con la misma referencia no crea duplicados: recibe la operación existente (código 200 en lugar de 201). Ante la duda (corte de red), reenvíe la misma solicitud.
Servicios digitales
Revenda en su sitio las licencias y activaciones de software GSM, los alquileres de herramientas por horas (UnlockTool, Chimera, TSM, AMT…), los créditos y los servicios IMEI / número de serie del catálogo de PlanetPay.
Funcionamiento
GET /v1/services: lee el catálogo (precios en USD, definitivos).POST /v1/services/{id}/orders: pide para su cliente; el precio se descuenta de su saldo en USD.- El resultado (código, credenciales, activación) llega en la respuesta, o más tarde por webhook.
- Si el proveedor rechaza o falla, su saldo se reembolsa automáticamente (
refunded: true).
GET /v1/services — catálogo
| Parámetro | Descripción |
|---|---|
category |
rental (alquiler de herramientas), licence (licencias y créditos), imei (servicios IMEI / SN) |
q |
búsqueda en el nombre y la herramienta |
page, limit |
paginación (limit: 50 por defecto, 100 máx.) |
{
"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
}]
}
| Campo | Descripción |
|---|---|
price |
precio unitario cobrado, en USD |
requires |
campos que se envían con el pedido: siempre identifier, los demás en fields |
quantity |
{ "min": 1, "max": 100 } para los servicios vendidos por cantidad (créditos), si no null |
delivery_time |
plazo orientativo anunciado por el proveedor |
GET /v1/services/{id} — un servicio
Mismo formato que un elemento de la lista. 404 si el servicio no existe o ya no se vende.
POST /v1/services/{id}/orders — pedir
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
identifier |
texto (120 máx.) | sí | identificador principal: usuario de la herramienta, IMEI, número de serie… |
fields |
objeto | según requires |
otros campos, p. ej. {"email": "cliente@mail.com"} |
quantity |
entero | no | servicios vendidos por cantidad (limitado a min / max) |
reference |
texto | sí | su referencia única (idempotencia) |
callback_url |
URL https | no | llamada en la entrega o el reembolso |
Respuesta 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 |
Significado |
|---|---|
pending |
pedido enviado al proveedor; el resultado llega con el webhook service_order.completed |
completed |
entregado: el resultado está en result |
failed |
rechazado o fallido: error indica el motivo, refunded vale true (saldo reintegrado) |
Seguimiento: GET /v1/transactions/{id} con el identificador ord_….
Números virtuales
Compre un número de teléfono temporal para recibir un código de verificación por SMS (WhatsApp, Telegram, Google, Facebook, TikTok…), en el país que elija. El precio se descuenta de su saldo en USD cuando se asigna el número.
Si no llega ningún SMS antes de que caduque el número (unos 20 minutos), o si usted cancela, el pedido se reembolsa automáticamente.
Funcionamiento
GET /v1/numbers/services: la lista de servicios (aplicaciones).GET /v1/numbers/services/{service}/countries: los países disponibles para ese servicio, con precio y stock.POST /v1/numbers: compra un número y se lo da a su cliente.- El código llega con el webhook
number.completed, o consultandoGET /v1/numbers/{id}cada 5 a 10 segundos.
GET /v1/numbers/services — servicios
{
"object": "list", "total": 2,
"data": [
{ "service": "telegram", "name": "Telegram" },
{ "service": "whatsapp", "name": "WhatsApp" }
]
}
Use el valor service en las llamadas siguientes.
GET /v1/numbers/services/{service}/countries — países y precios
{
"object": "list", "service": "whatsapp",
"data": [
{ "country": "usa", "name": "Estados Unidos", "price": 0.31, "currency": "USD", "stock": 340 },
{ "country": "france", "name": "Francia", "price": 0.61, "currency": "USD", "stock": 12 }
]
}
Los países van del más barato al más caro. price es el precio final cobrado; stock, los números disponibles en el momento de la consulta. Precios y stock cambian: vuelva a leerlos justo antes de comprar.
POST /v1/numbers — comprar un número
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
service |
texto | sí | valor service de /v1/numbers/services (p. ej. whatsapp) |
country |
texto | sí | valor country de /v1/numbers/services/{service}/countries (p. ej. france) |
reference |
texto | sí | su referencia única (idempotencia) |
callback_url |
URL https | no | llamada al recibir el SMS o en el reembolso |
Respuesta 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} — estado y código recibido
Devuelve el número con su estado actualizado (PlanetPay consulta al proveedor en cada llamada, como máximo una vez cada 5 segundos). Cuando llega el SMS:
{ "id": "num_hejqeog9olf0ufukhzii", "status": "completed", "sms": "Your WhatsApp code: 482-913", "code": "482913", "…": "…" }
status |
Significado |
|---|---|
pending |
número asignado, esperando el SMS |
completed |
SMS recibido: texto completo en sms, código extraído en code |
failed |
ningún SMS antes de expires_at, o cancelado: refunded vale true, reason indica la causa |
GET /v1/transactions/{id} devuelve lo mismo.
POST /v1/numbers/{id}/cancel — cancelar
Cancela un número que aún no ha recibido el SMS; su saldo se reintegra al instante (status: failed, refunded: true). Un número que ya recibió su SMS no se puede cancelar (409 already_completed).
Consejos: compre el número solo cuando su cliente esté listo para escribir el código; un número sirve para un solo código; si la aplicación rechaza el número, cancele y compre otro en otro país.
Tarjetas virtuales Visa
Cree tarjetas virtuales Visa o Mastercard en USD para usted o sus clientes, recárguelas y retire su saldo, desde su saldo PlanetPay. Las tarjetas sirven para pagos en línea (suscripciones, publicidad, comercio electrónico…).
Requisitos (los mismos que en la aplicación PlanetPay): cuenta verificada (KYC), perfil de titular con número de teléfono, plan de tarjetas activo, número máximo de tarjetas de su plan. Las comisiones de emisión, recarga y retiro de su plan se descuentan de su saldo en USD y aparecen en su cuenta.
Seguridad: la API nunca devuelve el número completo ni el CVV, solo los 4 últimos dígitos (
last4). El titular ve los datos completos en su cuenta PlanetPay, tras autenticarse.
Funcionamiento
POST /v1/cards: crea la tarjeta (se cobra la comisión de emisión).POST /v1/cards/{id}/fund: carga la tarjeta desde su saldo.- Pagos con la tarjeta.
POST /v1/cards/{id}/withdraw: devuelve el saldo restante de la tarjeta a su saldo.
El objeto tarjeta
{
"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"
}
| Campo | Descripción |
|---|---|
id |
identificador de la tarjeta (card_…), para las demás llamadas |
name |
nombre impreso en la tarjeta |
brand |
Visa o Mastercard |
last4, expiry |
4 últimos dígitos, caducidad MM/AA |
balance |
saldo de la tarjeta en USD |
status |
enabled = utilizable; cualquier otro valor = tarjeta bloqueada o cerrada |
GET /v1/cards — sus tarjetas
{ "object": "list", "mode": "live", "data": [ { "id": "card_…", "object": "card", "…": "…" } ] }
Con una clave de prueba la lista está vacía: las tarjetas de prueba solo existen en las respuestas de POST /v1/cards.
GET /v1/cards/{id} — una tarjeta
La tarjeta, con su saldo, sus 4 últimos dígitos y su caducidad leídos en directo en el emisor.
POST /v1/cards — crear una tarjeta
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
name_on_card |
texto (4 a 50) | sí | nombre impreso en la tarjeta |
brand |
visa | mastercard |
no | visa por defecto |
reference |
texto | sí | su referencia única (idempotencia) |
callback_url |
URL https | no | dirección de notificación |
Respuesta 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 — recargar
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
amount |
número (USD, 1 a 100 000) | sí | importe cargado en la tarjeta (las comisiones de recarga se suman) |
reference |
texto | sí | su referencia única |
callback_url |
URL https | no | dirección de notificación |
Respuesta 201 (cfd_…) con amount y la tarjeta actualizada en card.
POST /v1/cards/{id}/withdraw — retirar a su saldo
Mismos campos que la recarga. El importe vuelve a su saldo en USD (se aplican las comisiones de retiro de su plan). Respuesta 201 (cwd_…).
Errores de tarjetas
| HTTP | code |
Causa |
|---|---|---|
| 402 | insufficient_balance |
saldo PlanetPay (o saldo de la tarjeta, en un retiro) insuficiente |
| 403 | kyc_required |
clave de producción en una cuenta no verificada |
| 404 | not_found |
tarjeta desconocida o de otra cuenta |
| 422 | card_refused |
regla del plan no cumplida (sin plan, límite de tarjetas, perfil de titular o teléfono ausente, importe…); message indica el motivo |
| 502 | card_failed |
el emisor no pudo crear la tarjeta; revise su historial y vuelva a intentarlo más tarde |
Pago integrado (iframe)
Su cliente paga sin salir de su sitio: la ventana de PlanetPay se abre sobre su página (pantalla completa en móvil).
<script src="https://planetpay.site/js/planetpay.js"></script>
<button id="pay">Pagar</button>
<script>
document.getElementById('pay').onclick = async function () {
// Su servidor llama a POST /v1/recharge y devuelve payment_url (nunca ponga la clave secreta en el navegador)
const { payment_url } = await fetch('/mi-ruta/crear-pago', { method: 'POST' }).then(r => r.json());
PlanetPay.open(payment_url, {
onSuccess: function (e) { /* mostrar «gracias»; la entrega espera el webhook */ },
onFailure: function (e) { /* ofrecer reintentar */ },
onClose: function (e) { }
});
};
</script>
| Opción | Función |
|---|---|
onSuccess(e) |
el cliente pagó (e.reference, e.mode) |
onFailure(e) |
el pago falló |
onClose(e) |
se cerró la ventana |
autoClose |
cerrar automáticamente tras el éxito (true por defecto, autoCloseDelay = 2500 ms) |
closeOnBackdrop |
cerrar al pulsar fuera (true por defecto) |
Sin JavaScript, redirija a payment_url (con return_url) o use <iframe src="payment_url" allow="payment *">. La página de pago envía entonces a su página mensajes postMessage con la forma { "source": "planetpay", "event": "ready" | "success" | "failed" | "close", "reference": "…" }; compruebe que event.origin sea https://planetpay.site.
Importante: el evento del navegador solo sirve para la interfaz. Entregue un pedido solo tras el webhook firmado o GET /v1/transactions/{id}.
Plugin para SMM panel
¿Tiene un SMM panel (PHP + MySQL)? Sus clientes recargan su saldo con Mobile Money o tarjeta, en una ventana de PlanetPay dentro del panel, y el saldo se acredita automáticamente.
📦 Descargar el plugin (versión 2.1, PHP 7.4+, sin dependencias)
- Suba la carpeta
planetpaya la carpeta pública del panel (https://su-panel.com/planetpay/). - Ejecute
install.sqlen la base de datos del panel. - Copie
config.sample.phpcomoconfig.php: sus claves API, la base de datos, la tabla y columna del saldo (a menudousers/balance), la clave de sesión del cliente conectado. - En la página «Añadir fondos»:
<?php include $_SERVER['DOCUMENT_ROOT'] . '/planetpay/widget.php'; ?> - Añada la tarea cron de respaldo cada 5 minutos:
php /ruta/planetpay/cron.php(acredita los pagos cuyo webhook se perdió). - Pruebe con una clave de prueba, conectado con una cuenta incluida en
test_users(«Simular un pago correcto»), y luego pase a producción. Los pagos de prueba nunca se acreditan a otros clientes.
El plugin identifica al cliente por la sesión del panel, verifica la firma del webhook y la operación con la API, y acredita una sola vez el importe realmente pagado. Instrucciones completas en README.md.
Webhooks
Cuando una operación se confirma o falla, PlanetPay envía un POST JSON a su callback_url:
{ "event": "recharge.completed",
"data": { "id": "rch_…", "object": "recharge", "reference": "ORDER-1001", "status": "completed", "…": "…" } }
Eventos: recharge.completed, recharge.failed, transfer.completed, transfer.failed, service_order.completed, service_order.failed, number.completed (SMS recibido, código en data.code), number.failed (número reembolsado).
Verifique siempre la firma antes de entregar un pedido:
| Cabecera | Valor |
|---|---|
X-PlanetPay-Timestamp |
hora de envío (segundos Unix) |
X-PlanetPay-Signature |
HMAC-SHA256(clave_secreta, timestamp + "." + cuerpo_bruto) |
Responda con un código 2xx. Si no, reintentamos 5 veces (a los 10 s, 1 min, 5 min, 15 min y 1 h). Solo se llaman direcciones https públicas.
API de socios (plataformas conectadas)
Esta API es para empresas que ya tienen una plataforma en producción (bolsa, inversión, pagos) y quieren enviar sus operaciones reales a PlanetPay para que aparezcan en el mercado (cotizaciones, volúmenes, velas). No es un escaparate de proyectos: solo se publican datos verificables.
Conexión en 4 pasos
- Solicitud: en su cuenta PlanetPay, menú Conectar mi plataforma (empresa, sitio HTTPS, cumplimiento, URL de su API de auditoría).
- Prueba del dominio: registro DNS TXT
planetpay-verify=<token>(en@o_planetpay), leído mediante Google DNS-over-HTTPS, o un archivohttps://<dominio>/.well-known/planetpay-verify.txtcon el token. Se comprueba cada hora y luego cada día; prueba ausente 3 veces seguidas = conexión suspendida. - Revisión y fase de pruebas: el equipo de PlanetPay revisa el cumplimiento y abre la fase de pruebas (claves
pk_test_…). La aprobación exige un dominio verificado y una API de auditoría que haya respondido al menos una vez. - Producción: claves
pk_live_…, operaciones reales, doble verificación de cada operación.
El secreto de una clave (psk_…) se muestra una sola vez y permanece cifrado en nuestro lado. Dos claves activas como máximo por modo (rotación: cree la nueva, cambie, revoque la antigua).
Autenticación de socios
Base: https://planetpay.site/v1/partner (también en /api/v1/partner). Solo HTTPS, JSON UTF-8, 64 KB máx.
| Cabecera | Valor |
|---|---|
X-Partner-Key (or X-Partner-Id) |
pk_test_… / pk_live_… |
X-Timestamp (or X-Partner-Timestamp) |
hora Unix en segundos (±5 min) |
X-Signature (or X-Partner-Signature) |
hex(HMAC-SHA256(secret, timestamp + "." + METHOD + "." + path + "." + raw_body)) |
path = la ruta llamada, sin dominio ni parámetros (/v1/partner/events). Una firma se acepta una sola vez (protección contra repeticiones): firme de nuevo cada solicitud, reintentos incluidos. Límite: 300 solicitudes/min por clave.
Endpoints
| Método | Ruta | Uso |
|---|---|---|
| GET | /v1/partner/health |
estado del servicio, sin autenticación |
| GET | /v1/partner/status |
comprobación de conexión: socio, estado, modo, dominio, modo real permitido |
| POST | /v1/partner/events |
enviar un evento → 202 aceptado, 200 duplicado, 409 conflicto, 422 inválido |
| GET | /v1/partner/events/{event_id} |
estado de un evento enviado |
| GET | /v1/partner/events?status=&type=&limit=&before= |
lista (más recientes primero, cursor next_before) |
| GET | /v1/partner/assets |
sus activos conocidos por PlanetPay: estado (listado o no), último precio verificado, variación y volumen 24 h |
| GET | /v1/partner/transactions/{trade_id} |
una operación vista por PlanetPay: conservada o anulada, doble verificación, presencia en las cotizaciones, comisiones |
| GET | /v1/partner/commissions?status=&limit=&before= |
líneas de comisión (más recientes primero, cursor next_before) |
| GET | /v1/partner/reports?from=AAAA-MM-DD&to=AAAA-MM-DD |
informe del periodo (30 días por defecto, 366 máx.): eventos, operaciones, volumen por activo, comisiones, extractos, regla de tarifas |
Formato de un evento
{
"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.mi-plataforma.com/trades/TR-88412"
}
}
| type | obligatorio en 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 para anular una operación) |
asset.price_updated |
asset_id, price, currency (precio indicativo, no entra en las velas) |
Reglas: event_id único en su lado (reenviado idéntico = duplicado, otro contenido = 409); occurred_at ISO 8601 con zona horaria, no futuro, 30 días máx.; decimales positivos como cadenas (8 decimales máx.); trade_id identifica una ejecución (nunca se cuenta dos veces). Una orden nunca se trata como operación ejecutada. Cada activo pertenece a su plataforma: nunca se mezcla con las cotizaciones de otros socios.
Doble verificación (antifraude)
Una operación recibida no se publica de inmediato. PlanetPay llama a su API de auditoría y solo conserva la operación si existe en su sistema con el mismo activo, cantidad, precio y divisa, con el estado settled.
GET https://api.mi-plataforma.com/planetpay/audit/transaction/{id}
X-PlanetPay-Partner: ptn_…
X-PlanetPay-Timestamp: 1761643812
X-PlanetPay-Signature: hex(HMAC-SHA256(secreto_de_la_clave, timestamp + ".GET." + ruta_y_parametros + "."))
200 {"trade_id":"TR-88412","asset_id":"SNTS","quantity":"10","price":"25500","currency":"XOF","status":"settled"}
- Aún no
settled: se reintenta tras 1, 5, 15 min, 1, 3, 6 y 12 h, luego se abandona (no publicada). - 404 tres veces, cifras distintas, o estado
reversed/cancelled/failed: incoherencia, operación rechazada. - 3 incoherencias en 24 h: plataforma suspendida automáticamente.
- Solo HTTPS en su dominio verificado, sin redirección, respuesta < 8 s y < 16 KB.
Webhooks de socios (notificaciones de PlanetPay)
Configure una URL HTTPS en su dominio (pestaña Webhooks): PlanetPay le avisa en cuanto ocurre algo, sin necesidad de consultar la API.
| Evento | Cuándo |
|---|---|
event.accepted / event.rejected |
un evento enviado es aceptado / rechazado (con el error) |
trade.verified |
operación confirmada por la doble verificación, publicada |
trade.mismatch / trade.failed |
operación rechazada / nunca confirmada |
partner.status_changed |
cambió el estado de su plataforma |
asset.status_changed |
uno de sus activos listado o suspendido |
key.revoked |
una clave API revocada |
webhook.test |
prueba enviada desde la consola |
POST https://api.mi-plataforma.com/planetpay/webhooks
X-PlanetPay-Event: trade.verified
X-PlanetPay-Delivery: whd_… (idéntico en cada reintento: deduplique con él)
X-PlanetPay-Timestamp: 1761643815
X-PlanetPay-Signature: hex(HMAC-SHA256(whsec_…, timestamp + "." + raw_body))
{"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}}
Responda un 2xx en menos de 10 s (procese después en una cola). Si no, se reintenta tras 1, 5, 30 min, 2, 6, 12 y 24 h, luego falla (reenvío desde la consola). El secreto whsec_… es distinto de sus claves API y se muestra una sola vez.
Comisiones de PlanetPay
PlanetPay cobra una comisión por cada operación que usted declara, según su contrato: porcentaje de compra, porcentaje de venta y/o importe fijo por operación (en la moneda de la operación). La regla queda fijada en la línea cuando llega la operación: un cambio de tarifa nunca reescribe el historial y un evento recibido varias veces crea una sola línea.
| Estado | Significado |
|---|---|
pending |
operación aún no confirmada por la doble verificación, o dentro del plazo de anulación (3 días por defecto) |
earned |
devengada |
invoiced |
incluida en un extracto mensual |
paid |
extracto pagado |
cancelled |
operación anulada (transaction.reversed) o no confirmada (mismatch, failed) |
Las comisiones reales devengadas de un mes terminado se agrupan en un extracto mensual por moneda (PS…). Una operación anulada después de facturar genera una línea adjustment negativa en el extracto siguiente. Las comisiones del modo de prueba son solo una simulación y nunca se facturan. Detalle y exportación CSV: pestaña «Comisiones» de su panel.
Errores de socios
| 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 (guardado como rechazado; reenvíelo corregido con el mismo event_id) |
| 429 | rate_limited |
| 503 | service_unavailable |
Ejemplo completo (PHP)
// 1. enviar una operación ejecutada
$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. su API de auditoría (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. recibir nuestros 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()); // deduplique con el id
return response()->noContent();
});
Errores
Todos los errores tienen la misma forma:
{ "error": { "code": "insufficient_balance", "message": "Insufficient balance.", "required": 10.8 } }
| HTTP | code |
Causa |
|---|---|---|
| 401 | unauthorized |
faltan cabeceras, clave desconocida o firma inválida |
| 401 | timestamp_expired |
X-Timestamp desfasado más de 5 minutos |
| 403 | kyc_required |
clave de producción en una cuenta no verificada |
| 403 | account_disabled |
cuenta suspendida |
| 402 | insufficient_balance |
saldo insuficiente (transferencia + comisiones, servicio, número, tarjeta) |
| 404 | not_found |
operación desconocida |
| 409 | wallet_missing |
sin billetera en USD |
| 422 | validation_error |
campo inválido (fields detalla cada uno) |
| 422 | currency_not_supported |
moneda no disponible |
| 422 | operator_not_supported |
red o país no disponible para una transferencia |
| 422 | invalid_phone |
número inválido |
| 429 | rate_limited |
más de 120 solicitudes por minuto |
| 422 | order_refused |
pedido de servicio rechazado por el proveedor (no se cobra nada) |
| 422 | card_refused |
operación de tarjeta rechazada (plan, límite de tarjetas, importe…); el mensaje indica el motivo |
| 502 | card_failed |
el emisor no pudo crear la tarjeta |
| 404 | not_found |
servicio, número o tarjeta desconocido |
| 409 | number_unavailable |
ningún número disponible para ese servicio y país (no se cobra nada) |
| 409 | already_completed |
el número ya recibió su SMS: no se puede cancelar |
| 503 | service_unavailable |
números virtuales no disponibles temporalmente |
Ejemplos completos
Los ejemplos crean una recarga de 20 USD pagada en XOF. Sustituya PK y SK por sus claves.
cURL (bash)
PK="su_clave_api"; SK="su_clave_secreta"
BODY='{"amount":20,"currency":"XOF","reference":"ORDER-1001","callback_url":"https://susitio.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://susitio.com/planetpay/webhook',
});
console.log(recharge.payment_url);
// Webhook (Express): verificar la firma sobre el cuerpo BRUTO
// 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') { /* entregar el pedido event.data.reference */ }
// res.sendStatus(200);
// });
PHP
<?php
const PK = 'su_clave_api';
const SK = 'su_clave_secreta';
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);
// ... procesar $event['event'] y $event['data']['reference']
http_response_code(200);
Python 3
import hashlib, hmac, json, time, requests
PK, SK = "su_clave_api", "su_clave_secreta"
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
Servicios digitales, números virtuales y tarjetas Visa: PHP, Python, Node.js, Ruby, cURL
Cada ejemplo lista los alquileres de herramientas y pide uno, crea una tarjeta Visa y la recarga con 25 USD, y luego compra un número virtual de WhatsApp y espera el código SMS. Ejecútelos primero con una clave de prueba: no se cobra nada. Las claves se leen de las variables de entorno PLANETPAY_KEY y PLANETPAY_SECRET.
La ruta firmada nunca incluye los parámetros de la URL: para
GET /v1/services?category=rental, se firma/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"
Preguntas técnicas: contacte con soporte desde su cuenta PlanetPay indicando el identificador de la operación (rch_…, trf_…, ord_…, num_…, crd_……).
