MonCashConnect Connect

Connect permet à votre application d'exécuter des payouts MonCash sortants depuis le solde HTG d'un utilisateur MonCashConnect. Modèle mental : MonCashConnect est à un produit de remise ce qu'un réseau de cartes est à un commerçant. Votre application connaît à qui appartient l'argent et pourquoi ; MCC valide la source de fonds, vérifie la limite, et confirme que le rail a accepté.

Connect n'est pas pour tout le monde. C'est un accès réservé, activé par l'équipe, uniquement pour une application externe qui exécute des payouts depuis le solde d'autres titulaires de comptes MonCashConnect, avec leur consentement OAuth (cas type HaiPay). Si vous voulez simplement payer vos propres utilisateurs depuis votre solde, ce n'est pas Connect : utilisez l'API self-serve payout-create, décrite dans la doc marchand (aucune activation d'équipe requise, dès le KYC validé).
Cette page documente le contrat partenaire v5, en vigueur depuis le 17 mai 2026. La forme du fil (requêtes, réponses, payloads webhook) est identique à v4 ; v5 ajoute la réalité opérationnelle : latence de règlement et comportement lorsque la confirmation de livraison est différée. En cas de conflit, le comportement de l'API en production fait foi.

Qui possède quoi

Nom / CodeType / LimiteDescription
Authentification utilisateurMCCSupabase auth ; OAuth émis pour des utilisateurs MCC.
Autorisation par-payoutPartenaireVous collectez le consentement dans votre propre produit, puis demandez à MCC d'exécuter.
Solde HTGMCCTenu dans le ledger MCC. Débité à l'exécution.
Plafond journalier partenaireMCCuser_connected_apps.daily_limit_htg
Plafond plan utilisateurMCCplans.max_withdraw_htg_per_day
Plancher / plafond par-payoutMCC1 000 HTG plancher · 75 000 HTG plafond réseau
Exécution MonCashMCCVia l'exécuteur MCC, puis confirmation de livraison asynchrone.
Rapport de statut / webhooksMCCÉvénements signés HMAC vers votre URL enregistrée
Récompenses / fees / commissionsPartenaireMCC ne stocke ni n'écho aucun champ de récompense.
Identité expéditeurPartenaireMCC ne stocke ni n'écho. Stockez côté partenaire, indexé sur external_reference.

OAuth flow

RFC 6749 authorization-code flow avec authentification client HTTP Basic. Pas de refresh tokens. Codes uniques à usage unique, valides 60 secondes ; access tokens valides 30 jours.

1. Authorize

GET https://moncashconnect.com/connect/authorize
    ?response_type=code
    &client_id=<public_client_id>
    &redirect_uri=<exact match de callback_url>
    &scope=<scopes séparés par espace>
    &state=<opaque, contrôlé par le partenaire>

L'utilisateur signe in s'il ne l'est pas déjà, voit l'écran de consentement, choisit un plafond journalier, clique Autoriser. L'UX par défaut est gateway-only (requires_user_approval = false) ; l'utilisateur peut opter pour une seconde couche d'approbation, mais votre application ne doit pas en dépendre.

Sur approbation → redirect vers redirect_uri?code=<raw>&state=<state>. Sur refus → redirect vers redirect_uri?error=access_denied&state=<state>.

2. Échange du code contre un token

POST https://api.moncashconnect.com/v1/oauth-token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<raw>
&redirect_uri=<exact match de l'authorize redirect_uri>
{
  "access_token": "<43 caractères URL-safe>",
  "token_type":   "Bearer",
  "expires_in":   2592000,
  "scope":        "payout.request"
}

3. Scopes

Nom / CodeType / LimiteDescription
balance.readscopeLire le solde HTG de l'utilisateur et son plafond plan.
payout.requestscopeCréer des payouts au nom de l'utilisateur — requis pour external-payout-create.
payout.status.readscopeSonder external-payout-status pour les payouts créés sur cette connexion.
Réémettre un token pour le même user_connected_app_id révoque automatiquement le token actif précédent. Stockez un seul token par connexion à la fois côté partenaire.

Créer un payout

POST
https://api.moncashconnect.com/v1/external-payout-create

Corps canonique

Nom / CodeType / LimiteDescription
external_reference*string≤ 128 chars. Clé d'idempotence. Unique par user_connected_app_id.
amount_htg*integer1 000 ≤ value ≤ 75 000.
recipient_moncash_number*string8–15 chiffres. Préfixe + optionnel.
recipient_namestring≤ 200 chars. Affiché à l'utilisateur dans l'UI MCC.
descriptionstring≤ 280 chars. Affiché à l'utilisateur dans l'UI MCC.
metadataobjectArbitraire. ≤ 4 KB stringifié. Stocké tel quel sur l'EPR. Pas écho via webhook.

Parsing tolérant. Toute clé JSON hors de la liste canonique ci-dessus est lue et écartée. Pas de validation, pas d'erreur, pas de stockage, pas d'écho. Garantie de compatibilité pour partenaires migrant depuis v3.

Réponse

201 Created pour une nouvelle requête, 200 OK sur replay idempotent (matched external_reference) avec is_replay: true.

{
  "id":                          "5c715691-2953-4a35-8f72-ff817aefeb12",
  "status":                      "auto_approved",
  "amount_htg":                  1000,
  "currency":                    "HTG",
  "recipient_moncash_number_last4": "4567",
  "external_reference":          "smoke-stage6-1778177771",
  "is_sandbox":                  false,
  "is_replay":                   false,
  "created_at":                  "2026-05-07T18:16:13.131436+00:00",
  "expires_at":                  null
}

Idempotence — différence avec /pay-create

Les replays sont matchés sur (user_connected_app_id, external_reference). Un deuxième POST avec le même external_reference retourne l'état courant du payout original (200 OK, is_replay: true). Le corps du replay n'est pas consulté — les champs que vous auriez modifiés sont ignorés. Pour un retry avec des champs différents, générez un nouvel external_reference.

Convention différente sur le flux marchand /pay-create. Cet endpoint traite un referenceId dupliqué comme une erreur (409 Conflict), pas comme un replay. Codez vos retries en fonction.

Lire le statut

GET
https://api.moncashconnect.com/v1/external-payout-status?external_reference=<...>

Requiert le scope payout.status.read. Retourne la même forme que la partner-view (cf. §webhooks) pour tout payout créé précédemment sur cette connexion.

Réalité opérationnelle — confirmation de livraison asynchrone

Le réseau de règlement pour les payouts MonCash sortants n'expose pas de GET de statut fonctionnel. L'exécuteur MCC soumet le retrait puis attend la confirmation de livraison pour basculer la ligne. En pratique, un payout peut rester à submitting pendant des minutes ou des heures.

Ce n'est pas un bug. Reposez-vous sur les webhooks pour les transitions ; le polling n'est utile que pour réconcilier des événements manqués.

Limites de débit Connect

Nom / CodeType / LimiteDescription
POST /external-payout-create240/min IP600/min par app · 30/min par connexion
GET /external-payout-status240/min IP600/min par app · 60/min par connexion

Vocabulaire de statut

Valeurs de statut sur le fil

Nom / CodeType / LimiteDescription
pending_agent_approvalnon-terminalL'utilisateur a opté pour la 2e couche d'approbation et n'a pas agi.
agent_approvednon-terminalUtilisateur a approuvé (ou auto-approval s'applique). L'exécuteur va prendre le relais.
processing_by_moncashconnectnon-terminalMCC exécute activement ce payout.
submittingnon-terminalLe réseau de règlement a accepté la soumission. Confirmation de livraison en attente — peut prendre des heures.
completedterminalLivraison confirmée. La source de confirmation n'est pas exposée sur le fil.
failedterminalL'exécuteur ou le réseau de règlement a rejeté. Fonds non débités ou remboursés.
rejected_by_agentterminalUtilisateur a refusé sur l'écran de consentement.
cancelledterminalAnnulé avant exécution, ou connexion révoquée pendant que le payout était en attente.
expiredterminalFenêtre d'approbation de 24h dépassée sans action utilisateur.

Tous les statuts terminaux sont stables — une fois atteints, MCC ne changera plus le statut.

Horodatage de completed

Le occurred_at du webhook reflète quand MCC a marqué la ligne comme complétée, pas nécessairement quand la livraison MonCash réelle s'est produite. En cas de confirmation différée, les deux peuvent être à plusieurs heures d'écart. Traitez completed comme autoritatif pour l'état, pas comme un horodatage de livraison à la seconde près.

Webhooks

MCC POST des événements signés HMAC vers l'URL configurée dans connected_apps.webhook_url.

En-têtes envoyés

Nom / CodeType / LimiteDescription
X-MCC-Event-IduuidID unique de l'événement — utilisez-le pour la déduplication.
X-MCC-TimestampstringTimestamp Unix (secondes). Rejetez si > 5 min d'écart.
X-MCC-Signaturestringt=<unix>,v1=<hex(HMAC-SHA256(secret, "<unix>.<body>"))>
Content-Typestringapplication/json
Le schéma de signature Connect (t=…,v1=…) est différent de celui du flux marchand /pay-create (qui signe sha256=<hex> sur le corps brut, sans X-MCC-Event-Id). Si vous gérez les deux flux, écrivez deux vérificateurs.

Vérification de la signature

import crypto from "node:crypto";

// Lisez le corps en bytes bruts AVANT toute désérialisation
const rawBody  = Buffer.from(await req.text(), "utf8");
const sigHdr   = req.headers["x-mcc-signature"]; // "t=...,v1=..."
const eventId  = req.headers["x-mcc-event-id"];

const parts = Object.fromEntries(
  sigHdr.split(",").map((p) => p.split("=")),
);
const ts = parts.t, v1 = parts.v1;

if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
  throw new Error("timestamp drift");
}

const expected = crypto
  .createHmac("sha256", process.env.MCC_WEBHOOK_SECRET)
  .update(`${ts}.${rawBody.toString("utf8")}`)
  .digest("hex");

if (!crypto.timingSafeEqual(
  Buffer.from(expected, "hex"),
  Buffer.from(v1, "hex"),
)) throw new Error("bad signature");

// Dédupliquez sur eventId avant de traiter
if (await alreadyProcessed(eventId)) return res.sendStatus(200);

Événements

Nom / CodeType / LimiteDescription
external_payout.createdlifecycleEPR inséré.
external_payout.agent_approvedlifecycleStatut passé à agent_approved (ou auto_approved à l'insertion).
external_payout.submitted_to_baziklifecycleLe réseau de règlement a accepté la soumission ; en attente de confirmation de livraison. (data.status = « submitting »)
external_payout.completedterminalLivraison confirmée. Source non exposée sur le fil.
external_payout.failedterminalÉchec exécuteur ou livraison. Fonds non débités / remboursés.
external_payout.rejectedterminalUtilisateur a refusé sur l'écran de consentement.
external_payout.cancelledterminalAnnulé, y compris annulation par révocation de connexion.
external_payout.expiredterminalFenêtre d'approbation de 24h dépassée.

Exemple de payload

external_payout.completed
L'enveloppe (event_id, event_type, occurred_at) entoure une partner-view dans data.
{
  "event_id": "f8e2d1c0-1234-5678-9abc-def012345678",
  "event_type": "external_payout.completed",
  "occurred_at": "2026-05-07T18:16:13Z",
  "data": {
    "id": "5c715691-2953-4a35-8f72-ff817aefeb12",
    "external_reference": "smoke-stage6-1778177771",
    "status": "completed",
    "amount_htg": 1000,
    "currency": "HTG",
    "recipient_moncash_number_last4": "4567",
    "recipient_name": "Camy Peter",
    "is_sandbox": false,
    "created_at": "2026-05-07T18:14:02Z",
    "updated_at": "2026-05-07T18:16:13Z",
    "delivery_confirmed_at": "2026-05-07T18:16:13Z",
    "delivery_failed_at": null,
    "failure_reason": null
  }
}

Vocabulaire de failure_reason

Quand data.status = "failed", ce champ porte l'une de :

Nom / CodeType / LimiteDescription
executor_errorinterne MCCÉchec de l'exécuteur avant soumission au réseau de règlement. Fonds remboursés.
delivery_failedréseau/MonCashLe réseau de règlement ou MonCash a rejeté la livraison. Fonds remboursés.
connection_revokedutilisateurUtilisateur a révoqué la connexion pendant le traitement. Fonds remboursés.
insufficient_balanceutilisateurSolde MCC insuffisant à l'exécution (rare — attrapé à l'intake).
below_minimumvalidationMontant en-dessous du plancher 1 000 HTG à l'exécution.
rejected_by_agentutilisateurUtilisateur a refusé sur l'écran de consentement.
failedlegacyNon spécifié / legacy. La ligne est terminale ; traitez comme signal souple.

Sémantique de livraison

  • At-least-once. Une 2xx du partenaire stoppe les retries ; sinon retry exponentiel jusqu'à un cap d'attempts.
  • Livraison hors-ordre possible. Triez par occurred_at si vous suivez les transitions ; traitez data.status comme autoritatif.
  • Dédupliquez sur event_id. Même id = même événement, même si le payload diffère entre tentatives.
  • Réception idempotente. Traiter deux fois le même event_id doit produire le même résultat.

Attentes opérationnelles

Le fil donne le statut, pas la latence. Voici ce qui est normal et ce qui ne l'est pas.

SLA happy-path typique

Nom / CodeType / LimiteDescription
created → agent_approvedinstantanéAuto-approval applique dans la même requête de création.
agent_approved → submitting0–60sL'exécuteur prend la file ; reconciler Phase B couvre les retards.
submitting → completed0–10 minQuand la confirmation de livraison arrive rapidement (cas courant).
submitting → completed (long)10 min – plusieurs heuresQuand la confirmation de livraison tarde ; résolu par réconciliation.

Que faire si un payout reste à submitting ≥ 30 min

  • N'annulez pas, ne réessayez pas. Les fonds sont déjà débités du solde MCC de l'utilisateur ; le payout sera complété (via admin_manual si nécessaire) ou remboursé.
  • Affichez "en cours" côté UX, pas "échoué". Le statut sur le fil reste non-terminal jusqu'à résolution.
  • Contactez le support MCC avec le external_reference si la latence dépasse votre fenêtre tolérable. Le support vérifiera indépendamment et confirmera la livraison si elle a bien eu lieu.

Codes d'erreur

Toutes les erreurs renvoient un objet JSON avec un champ error (code machine-stable) et un champ error_description (humain).

Nom / CodeType / LimiteDescription
invalid_request400Corps mal formé, champ canonique manquant, valeur hors plage. error_description explique.
amount_below_minimum400amount_htg < 1 000.
invalid_token401Bearer manquant, token inconnu, révoqué ou expiré.
insufficient_scope403Le token n'a pas le scope requis (payout.request, payout.status.read).
connection_revoked403L'utilisateur a révoqué la connexion côté MCC après émission du token.
connection_not_found404Le user_connected_app_id du token n'existe plus.
partner_daily_cap_exceeded409Plafond journalier par-user de votre application dépassé. NE PAS retry avec le même external_reference.
plan_daily_cap_exceeded409Plafond du plan de l'utilisateur dépassé.
rate_limit_exceeded429Par-IP (240/min), par-app (600/min), ou par-connexion (30/min).
internal_error500Bug MCC. Safe à retry avec le même external_reference.
auth_unavailable503Infra transitoire. Safe à retry.
rate_limit_unavailable503Infra transitoire. Safe à retry.
{
  "error":             "insufficient_scope",
  "error_description": "Token lacks 'payout.request' scope"
}