Référence publique des endpoints REST de paiement, d’intégration et de portefeuille personnel d’OrionPay. URL de base : https://payapi.orion.moe
Un index lisible par LLM se trouve à /llms.txt et la spécification complète à /llms-full.txt. La spécification OpenAPI 3.1 se trouve à payapi.orion.moe/openapi.json. Les notes de version du produit et de l’API se trouvent à /changelog.
Un serveur MCP Streamable HTTP avec 18 outils est en ligne à payapi.orion.moe/mcp
claude mcp add orionpay --transport http https://payapi.orion.moe/mcp --header "X-API-Key: $ORIONPAY_API_KEY"Générez une clé API dans Dashboard > API & Webhooks > MCP / LLM.
Chaque endpoint public exige une clé API envoyée dans l’en-tête X-API-Key (ou Authorization: Bearer). Il en existe deux types :
productId = null. Contrôle le portefeuille de l’utilisateur (solde, retraits, swap). Ne crée pas de paiements de vente.
Endpoints : /api/v1/personal/*.
Liée à un produit. Nécessaire pour vendre via l’API : elle crée les paiements et déclenche les webhooks de vente.
Endpoints : /api/v1/pix/generate, /integration/*.
X-API-Key: opay_abc123def456...// Générer un PIX via l’API
const response = await fetch('https://payapi.orion.moe/api/v1/pix/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'opay_sua_chave_aqui'
},
body: JSON.stringify({
amount: 199.90,
name: 'João da Silva',
email: '[email protected]'
})
})
const { data } = await response.json()
console.log(data.purchaseId) // ID interne de l’achat
console.log(data.pixCode) // PIX copier-coller
console.log(data.qrCode) // QR Code = pixCode
console.log(data.expiresAt) // ISO 8601Pour vendre via l’API
Les paiements de checkout (/api/v1/pix/generate) et les webhooks de vente (purchase.created, payment.success) exigent une Clé API de produit — créez un produit dans le tableau de bord et générez sa clé. La clé API personnelle n’accède qu’à votre portefeuille (/api/v1/personal/*) et ne crée aucun paiement. Les dépôts sur votre propre compte se font depuis le tableau de bord, pas via l’API.
/api/v1/pix/generateClé API de produitGénère un QR Code PIX lié à un produit. Utilise le prix du produit ou le montant transmis. Le CPF/CNPJ du payeur (champ cpf) est obligatoire depuis le 30/06/2026 — alternative : euid (identifiant du payeur sur DePix).
Corps de la requête
{
"amount": 199.90,
"description": "Venda de produto",
"name": "João da Silva",
"email": "[email protected]",
"cpf": "52998224725",
"phone": "+5511999999999"
}Réponse (200)
{
"success": true,
"data": {
"purchaseId": 12345,
"productId": 8,
"productTitle": "Exemplo Produto",
"id": "550e8400-e29b-41d4-a716-446655440000",
"pixCode": "00020126580014br.gov.bcb.pix...",
"qrCode": "00020126580014br.gov.bcb.pix...",
"amount": 199.90,
"description": "Venda de produto: Exemplo Produto",
"expiresAt": "2026-04-24T14:30:00.000Z",
"status": "PENDING",
"coProducers": [
{ "userId": 5, "name": "Co-produtor Name", "percentage": 15.50 }
]
}
}/api/v1/pix/status/:idN’importe quelle clé API60 req/minAccepte purchaseId (numérique), transactionId (numérique) ou eulenDepositId (UUID).
Réponse (200)
{
"success": true,
"type": "product_purchase",
"id": 12345,
"eulenDepositId": "550e8400-e29b-41d4-a716-446655440000",
"status": "PAID",
"paidAt": "2026-04-24T12:00:00.000Z",
"accessToken": "abcd1234efgh5678ijkl9012mnop3456",
"productId": 8,
"productTitle": "Exemplo Produto",
"amount": 199.90,
"buyerEmail": "[email protected]",
"buyerName": "João Silva",
"createdAt": "2026-04-24T10:00:00.000Z"
}/integration/verify-accessClé API de produit60 req/minQuery : email ou accessToken (au moins un obligatoire).
Réponse (200)
{
"hasAccess": true,
"purchaseId": 12345,
"accessToken": "abcd1234efgh5678ijkl9012mnop3456",
"buyerEmail": "[email protected]",
"buyerName": "João Silva",
"productTitle": "Exemplo Produto",
"purchaseDate": "2026-04-20T10:00:00.000Z",
"expiresAt": "2026-05-20T10:00:00.000Z"
}/integration/grant-accessClé API de produitCrée un achat déjà payé pour l’e-mail indiqué. Utile pour les invitations et le support.
Corps de la requête
{
"buyerEmail": "[email protected]",
"buyerName": "Maria Santos",
"price": 199.90,
"expiresAt": "2026-05-24T00:00:00.000Z"
}Réponse (200)
{
"success": true,
"message": "Acesso concedido com sucesso",
"purchaseId": 12347,
"accessToken": "abcd1234efgh5678ijkl9012mnop3456",
"buyerEmail": "[email protected]",
"buyerName": "Maria Santos",
"expiresAt": "2026-05-24T00:00:00.000Z"
}/integration/buyersClé API de produit60 req/minQuery facultatif : status (PENDING, PAID, EXPIRED).
Réponse (200)
{
"total": 2,
"buyers": [
{
"purchaseId": 12345,
"buyerEmail": "[email protected]",
"buyerName": "João Silva",
"price": 199.90,
"paymentStatus": "PAID",
"accessToken": "abcd1234efgh5678ijkl9012mnop3456",
"purchaseDate": "2026-04-20T10:00:00.000Z",
"paidAt": "2026-04-20T10:05:00.000Z",
"expiresAt": "2026-05-20T10:00:00.000Z",
"paymentLink": "Link de Pagamento Exemplo"
}
]
}/integration/buyers/:purchaseIdClé API de produitRenvoie les données complètes d’un achat par son ID.
Réponse (200)
{
"purchaseId": 12345,
"buyerEmail": "[email protected]",
"buyerName": "João Silva",
"price": 199.90,
"paymentStatus": "PAID",
"accessToken": "abcd1234efgh5678ijkl9012mnop3456",
"purchaseDate": "2026-04-20T10:00:00.000Z",
"paidAt": "2026-04-20T10:05:00.000Z",
"expiresAt": "2026-05-20T10:00:00.000Z",
"product": { "id": 8, "title": "Exemplo Produto", "productType": "COURSE" },
"paymentLink": { "id": 1, "title": "Link de Pagamento Exemplo" }
}/integration/contentClé API de produit60 req/minQuery : accessToken (obligatoire). Valide le jeton et renvoie les contenus débloqués.
Réponse (200)
{
"productId": 8,
"buyerEmail": "[email protected]",
"buyerName": "João Silva",
"contents": [
{
"id": 1,
"type": "video",
"title": "Aula 1 - Introdução",
"url": "https://storage.example.com/video1.mp4",
"fileSize": 524288000,
"mimeType": "video/mp4",
"order": 1,
"metadata": "{\"duration\": 3600}"
}
]
}/integration/revoke-access/:purchaseIdClé API de produitInvalide l’accessToken de l’achat indiqué.
Réponse (200)
{
"success": true,
"message": "Acesso revogado com sucesso",
"purchaseId": 12345
}/api/v1/integration/deposit/pixClé API de produit10 req/minCrée un QR Code PIX pour le produit. Le CPF/CNPJ du payeur (champ cpf, chiffres uniquement) est obligatoire depuis le 30/06/2026 — alternative : euid (identifiant du payeur sur DePix). L’identifiant du marchand (merchantId) est automatique (résolu via votre clé API) ; inutile de l’envoyer.
Corps de la requête
{
"cpf": "52998224725",
"amount": 199.90,
"buyerEmail": "[email protected]",
"buyerName": "João da Silva"
}Réponse (200)
{
"purchaseId": 12348,
"productId": 8,
"productTitle": "Exemplo Produto",
"id": "550e8400-e29b-41d4-a716-446655440000",
"pixCode": "00020126580014br.gov.bcb.pix...",
"qrCode": "00020126580014br.gov.bcb.pix...",
"amount": 199.90,
"expiresAt": "2026-04-24T14:30:00.000Z",
"status": "created"
}/api/v1/personal/balanceClé API personnelle60 req/hRenvoie le solde par devise, y compris bloqué et en attente.
Réponse (200)
{
"success": true,
"wallets": [
{
"currency": "BRL",
"balance": 5000.00,
"blockedBalance": 500.00,
"pendingBalance": 0.00,
"availableBalance": 4500.00
},
{
"currency": "L-BTC",
"balance": 0.02,
"blockedBalance": 0.00,
"pendingBalance": 0.00,
"availableBalance": 0.02
}
]
}/api/v1/personal/limitsClé API personnelle30 req/hRenvoie les limites quotidiennes/mensuelles et la consommation actuelle.
Réponse (200)
{
"success": true,
"limits": {
"dailyWithdrawBRL": 10000.00,
"monthlyWithdrawBRL": 100000.00,
"dailyWithdrawCrypto": 2.00,
"monthlyWithdrawCrypto": 10.00,
"dailyUsed": 2000.00,
"monthlyUsed": 15000.00
}
}/api/v1/personal/transactionsClé API personnelle60 req/hQuery : limit, offset, type (DEPOSIT, WITHDRAWAL, SWAP).
Réponse (200)
{
"success": true,
"transactions": [
{
"id": 789,
"type": "DEPOSIT",
"status": "COMPLETED",
"amount": 500.00,
"description": "Depósito PIX",
"blockchainTxId": null,
"createdAt": "2026-04-24T10:00:00.000Z"
}
],
"pagination": { "total": 50, "limit": 10, "offset": 0, "hasMore": true }
}/api/v1/personal/withdraw/quoteClé API personnelle120 req/hSimule un retrait avec le même calcul que le débit réel. Envoyez exactement un des deux : ?amount= (brut à débiter) OU ?netAmount= (net souhaité sur le compte de destination). grossAmount − totalFee = netAmount tombe juste.
Réponse (200)
{
"success": true,
"quote": { "grossAmount": 78.31, "totalFee": 3.31, "netAmount": 75.00, "requestedNetAmount": 75.00 },
"limits": { "minAmount": 4.00, "maxAmount": 50000.00 },
"validation": { "withinLimits": true }
}/api/v1/personal/withdraw/pixClé API personnelle10 req/hEnvoyez exactement un des deux : amount (1–100 000, brut à débiter) OU netAmount (net qui doit arriver sur le compte — le débit est calculé automatiquement). fee est le frais TOTAL et amount − fee = netAmount. pixKeyType est détecté automatiquement (EMAIL, CPF, CNPJ, PHONE, RANDOM).
Corps de la requête
{
"netAmount": 75.00,
"pixKey": "12345678000123",
"pixKeyType": "CPF",
"description": "Saque via API",
"taxNumber": "52998224725"
}Réponse (200)
{
"success": true,
"message": "Saque PIX em processamento",
"queueId": 88,
"transactionId": 791,
"status": "PENDING",
"requiresApproval": false,
"amount": 78.31,
"fee": 3.31,
"total": 78.31,
"netAmount": 75.00,
"requestedNetAmount": 75.00,
"estimatedProcessingTime": "1-2 minutos"
}/api/v1/personal/withdraw/cryptoClé API personnelle10 req/hPrend en charge L-BTC, BTC-PEGOUT, DePix, USDt, USDC. memo obligatoire pour TON.
Corps de la requête
{
"amount": 0.01,
"toAddress": "bc1q6y3kz2c8s4n9p1m0l2k3j4h5g6f7e8d9c0b1a",
"assetTicker": "BTC-PEGOUT",
"network": "bitcoin"
}Réponse (200)
{
"success": true,
"transactionId": 792,
"status": "PENDING",
"amount": 0.01,
"assetTicker": "BTC-PEGOUT",
"toAddress": "bc1q6y3kz2c8s4n9p1m0l2k3j4h5g6f7e8d9c0b1a",
"blockchainTxId": null,
"createdAt": "2026-04-24T14:00:00.000Z"
}/api/v1/personal/withdraw/status/:idClé API personnelle120 req/hRenvoie l’état actuel, les frais totaux et le montant net. netAmount devient le paiement confirmé par le prestataire dès qu’il est disponible (payoutConfirmed: true). Pour les retraits crypto, toAddress/asset remplacent pixKey.
Réponse (200)
{
"success": true,
"transaction": {
"id": 791,
"status": "COMPLETED",
"amount": 78.31,
"fee": 3.31,
"netAmount": 75.00,
"payoutConfirmed": true,
"requestedNetAmount": 75.00,
"pixKey": "12345678000123",
"description": "P-API: Saque PIX",
"blockchainTxId": null,
"errorCode": null,
"errorMessage": null,
"createdAt": "2026-04-24T14:00:00.000Z",
"updatedAt": "2026-04-24T14:05:00.000Z"
}
}/api/v1/personal/swap/quoteClé API personnelle120 req/hDevises prises en charge : DePix, L-BTC, USDt, EURx. Le devis est valable 5 minutes.
Corps de la requête
{
"fromCurrency": "BRL",
"toCurrency": "BTC",
"amount": 1000.00
}Réponse (200)
{
"success": true,
"fromCurrency": "BRL",
"toCurrency": "BTC",
"fromAmount": 1000.00,
"toAmount": 0.0195,
"rate": 0.0000195,
"fee": 5.00,
"feeCurrency": "BRL",
"expiresAt": "2026-04-24T14:15:00.000Z"
}/api/v1/personal/swap/executeClé API personnelle20 req/hDébite fromCurrency et crédite toCurrency au prix en vigueur au moment de l’exécution.
Corps de la requête
{
"fromCurrency": "BRL",
"toCurrency": "BTC",
"amount": 1000.00
}Réponse (200)
{
"success": true,
"transactionId": 793,
"fromCurrency": "BRL",
"toCurrency": "BTC",
"fromAmount": 1000.00,
"toAmount": 0.0195,
"status": "COMPLETED",
"executedAt": "2026-04-24T14:05:00.000Z"
}/payment-links/purchase/:purchaseId/statusPublic (sans auth)60 req/minLe seul endpoint sans authentification. Accepte un ID numérique (achat) ou un UUID (eulenDepositId). Prévu pour le polling au checkout.
Réponse (200)
{
"purchaseId": 12345,
"paymentStatus": "PAID",
"paidAt": "2026-04-24T12:00:00.000Z",
"accessToken": "abcd1234efgh5678ijkl9012mnop3456",
"productTitle": "Exemplo Produto",
"price": 199.90
}En cas de dépassement, le backend répond HTTP 429. Les limites sont par clé API + route et se réinitialisent dans la fenêtre indiquée.
Valeurs possibles du champ status sur la ressource Transaction (GET /transactions/:id)
PIX généré, en attente de paiement. Le solde n'a pas encore été déplacé.
Eulen en cours de traitement. Peut être under_review (anti-fraude), delayed (Eulen retient le DePix avant l'approbation — aucun effet financier pour le moment) ou en delay-depix HOLD après l'approved (PIX confirmé et blockedBalance déjà incrémenté ; sous-phase identifiée par metadata.holdSource = "delay-depix" + metadata.approvedAt + availableAt). Webhook de cette sous-phase : deposit.approved_delay.
DePix livré dans la réserve. Solde crédité dans balance (ou dans pendingBalance si isOnHold=true). Webhook émis : deposit.success ou deposit.on_hold.
Échec du PIX (non payé, expiré, refusé par Eulen ou la banque). Aucun solde déplacé.
Transaction annulée manuellement (admin) ou par TTL (PIX non payé dans la fenêtre).
Remboursement : MED (rétrofacturation PIX) détecté pendant la rétention ou remboursement manuel par admin. Solde retiré de blockedBalance ou pendingBalance selon metadata.holdSource.
Transaction en revue manuelle par l'équipe. En attente d'approbation ou de rejet.
Recevez des notifications automatiques lorsque des événements importants se produisent
Chaque webhook porte une signature HMAC-SHA256 dans l’en-tête X-Webhook-Signature, calculée sur le corps brut (raw body) avec le secret du webhook. Validez toujours avant de traiter.
X-Webhook-Signature: a1b2c3d4e5f6...payment.successPaiement confirmé et crédité.
{
"event": "payment.success",
"productId": 8,
"productTitle": "Exemplo Produto",
"webhookName": "notificar_sistema",
"timestamp": "2026-04-24T12:00:00.000Z",
"data": {
"purchaseId": 12345,
"transactionId": "txn_abc123",
"productId": 8,
"buyerEmail": "[email protected]",
"buyerName": "João Silva",
"accessToken": "abcd1234efgh5678ijkl9012mnop3456",
"price": 199.90,
"netAmount": 179.91,
"platformFee": 19.99,
"timestamp": "2026-04-24T12:00:00.000Z"
}
}purchase.createdAchat créé, en attente de paiement.
{
"event": "purchase.created",
"productId": 8,
"timestamp": "2026-04-24T10:00:00.000Z",
"data": {
"purchaseId": 12345,
"transactionId": "txn_abc123",
"amount": 199.90,
"status": "pending",
"createdAt": "2026-04-24T10:00:00.000Z",
"buyerEmail": "[email protected]",
"buyerName": "João Silva"
}
}access.grantedAccès accordé à l’acheteur après confirmation du paiement.
{
"event": "access.granted",
"productId": 8,
"timestamp": "2026-04-24T12:00:05.000Z",
"data": {
"purchaseId": 12345,
"buyerEmail": "[email protected]",
"accessToken": "abcd1234efgh5678ijkl9012mnop3456",
"expiresAt": "2026-05-24T12:00:05.000Z",
"grantedAt": "2026-04-24T12:00:05.000Z"
}
}payment.refunded_medPaiement remboursé via MED (rétrofacturation) pendant la rétention antifraude.
{
"event": "payment.refunded_med",
"productId": 8,
"timestamp": "2026-04-24T15:00:00.000Z",
"data": {
"transactionId": 789,
"amount": 199.90,
"reference": "550e8400-e29b-41d4-a716-446655440000",
"status": "refunded",
"type": "DEPOSIT",
"currency": "BRL",
"reason": "med_during_antifraud_hold",
"medDetectedAt": "2026-04-24T14:50:00.000Z",
"refundedAt": "2026-04-24T15:00:00.000Z"
}
}payment.refundedDépôt remboursé par Eulen au payeur (PIX retourné) ; le montant retenu est contre-passé. Un remboursement MED de fonds déjà crédités arrive comme payment.refunded_med. Le champ facultatif refundReasonDetail porte le motif déduit (med_chargeback, cpf_mismatch, cnpj_mismatch, spam_like, user_blocked, payer_refused, indeterminate_payer). Sauf pour indeterminate_payer, la charge utile inclut payerEuid — identifiant pseudonyme et stable du compte payeur, pour corréler les récidivistes sans révéler l’identité. Les motifs mismatch portent aussi payerName et payerTaxNumber, le CPF étant toujours masqué.
{
"event": "payment.refunded",
"productId": 8,
"timestamp": "2026-04-24T15:00:00.000Z",
"data": {
"transactionId": 789,
"amount": 199.90,
"reference": "550e8400-e29b-41d4-a716-446655440000",
"status": "refunded",
"type": "DEPOSIT",
"currency": "BRL",
"reason": "eulen_refund",
"refundReasonDetail": "cpf_mismatch",
"payerEuid": "EU015562310201680",
"payerName": "João da Silva",
"payerTaxNumber": "***.456.789-**",
"eulenStatus": "refunded",
"refundedAt": "2026-04-24T15:00:00.000Z",
"credited": false
}
}payment.refund_pendingAvis PRÉALABLE : Eulen a annoncé qu’elle va restituer un PIX déjà reçu (risque/conformité). Ce n’est pas l’événement terminal — payment.refunded suit, en moyenne ~2 h et jusqu’à 3 jours lorsque le prestataire retient le montant au préalable (indiqué dans expectedRefundAt). Considérez-le comme irréversible. Le champ facultatif rejectionReasons porte le code brut du prestataire (PAYER_MISMATCH, BLOCKED_USER) ; le vocabulaire est le sien et peut s’étoffer — traitez un code inconnu comme un refus de risque générique. Si l’acheteur avait déjà accès, OrionPay le révoque et envoie accessRevoked: true ; annulez la livraison de votre côté également.
{
"event": "payment.refund_pending",
"productId": 8,
"timestamp": "2026-04-24T15:00:00.000Z",
"data": {
"transactionId": 789,
"amount": 199.90,
"reference": "550e8400-e29b-41d4-a716-446655440000",
"status": "refund_pending",
"type": "DEPOSIT",
"currency": "BRL",
"reason": "eulen_will_refund",
"rejectionReasons": ["PAYER_MISMATCH"],
"expectedRefundAt": "2026-04-27T15:00:00.000Z",
"payerEuid": "EU015562310201680",
"eulenStatus": "will_refund",
"credited": false,
"accessRevoked": true,
"purchaseId": 12345,
"productId": 8,
"productTitle": "Exemplo Produto",
"buyerEmail": "[email protected]",
"buyerName": "Cliente Exemplo"
}
}med.createdMED (rétrofacturation) détecté sur un dépôt Eulen.
{
"event": "med.created",
"timestamp": "2026-04-24T14:50:00.000Z",
"data": {
"qrId": "550e8400-e29b-41d4-a716-446655440000",
"bankTxId": "E00000000202604241450",
"amount": 199.90,
"currency": "BRL",
"medDetectedAt": "2026-04-24T14:50:00.000Z",
"depositStatus": "depix_sent",
"payerTaxNumber": "52998224725",
"transactionId": 789,
"purchaseId": 12345,
"productId": 8,
"productTitle": "Exemplo Produto"
}
}withdrawal.completedRetrait transmis à la blockchain ou à la banque.
{
"event": "withdrawal.completed",
"timestamp": "2026-04-24T14:05:00.000Z",
"data": {
"withdrawalId": 792,
"amount": 0.01,
"fee": 0.00001,
"totalDeducted": 0.01001,
"asset": "L-BTC",
"toAddress": "lq1qqw...",
"txHash": "abc123...",
"network": "liquid",
"isSideShift": false,
"completedAt": "2026-04-24T14:05:00.000Z"
}
}withdrawal.settledRetrait entièrement confirmé sur le réseau de destination (Peg-Out BTC inclus).
{
"event": "withdrawal.settled",
"timestamp": "2026-04-24T14:30:00.000Z",
"data": {
"withdrawalId": 792,
"amount": 0.01,
"asset": "BTC-PEGOUT",
"toAddress": "bc1q...",
"liquidTxHash": "abc123...",
"settleTxHash": "def456...",
"settleNetwork": "bitcoin",
"completedAt": "2026-04-24T14:30:00.000Z"
}
}withdrawal.failedÉchec du retrait (solde insuffisant, adresse invalide, etc.). Les montants sont déjà revenus au solde.
{
"event": "withdrawal.failed",
"timestamp": "2026-04-24T14:05:00.000Z",
"data": {
"withdrawalId": 792,
"amount": 0.01,
"asset": "L-BTC",
"toAddress": "lq1qqw...",
"network": "liquid",
"error": "Insufficient hotwallet balance",
"failedAt": "2026-04-24T14:05:00.000Z"
}
}swap.completedSwap atomique exécuté avec succès.
{
"event": "swap.completed",
"timestamp": "2026-04-24T14:05:00.000Z",
"data": {
"txid": "abc123...",
"fromCurrency": "L-BTC",
"toCurrency": "DePix",
"fromAmount": 0.01,
"toAmountGross": 3500.00,
"toAmountNet": 3482.50,
"feePercent": 0.5,
"feeAmount": 17.50,
"completedAt": "2026-04-24T14:05:00.000Z"
}
}swap.failedLe swap a échoué ou a été remboursé.
{
"event": "swap.failed",
"timestamp": "2026-04-24T14:05:00.000Z",
"data": {
"transactionId": 793,
"fromCurrency": "L-BTC",
"toCurrency": "DePix",
"fromAmount": 0.01,
"step": "REFUNDED",
"reason": "Manual refund by admin",
"refundedAt": "2026-04-24T14:05:00.000Z"
}
}webhook.testÉvénement déclenché manuellement depuis le tableau de bord pour tester l’URL.
{
"event": "webhook.test",
"timestamp": "2026-04-24T14:05:00.000Z",
"data": {
"message": "Webhook de teste",
"testId": "test_abc123"
}
}// Valider la signature du webhook (Node.js)
const crypto = require('crypto')
function validateWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
)
}
app.post('/webhooks/orionpay', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature']
const rawBody = req.body.toString('utf8')
if (!validateWebhook(rawBody, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' })
}
const { event, data } = JSON.parse(rawBody)
switch (event) {
case 'payment.success':
grantAccess(data.buyerEmail, data.accessToken)
break
case 'payment.refunded_med':
case 'payment.refunded':
case 'payment.refund_pending':
revokeAccess(data.transactionId)
break
case 'withdrawal.settled':
markWithdrawalSettled(data.withdrawalId)
break
}
res.status(200).json({ received: true })
})