MonCashConnect Connect

Connect lets your application execute outbound MonCash payouts from the HTG balance of a MonCashConnect user. Mental model: MonCashConnect is to a remittance product what a card network is to a merchant. Your app knows whose money it is and why; MCC validates the source of funds, checks the limit, and confirms the rail accepted.

Connect isn't for everyone. It's a gated, team-activated access, only for an external application that executes payouts from the balance of other MonCashConnect account holders, with their OAuth consent (HaiPay-style). If you just want to pay your own users from your own balance, this isn't Connect: use the self-serve payout-create API, documented in the merchant docs (no team activation required, as soon as KYC is approved).
This page documents the partner contract v5, in effect since May 17, 2026. The wire shape (requests, responses, webhook payloads) is identical to v4; v5 adds operational reality: settlement latency and behavior when delivery confirmation is delayed. In case of conflict, the production API behavior is authoritative.

Who owns what

Nom / CodeType / LimiteDescription
User authenticationMCCSupabase auth; OAuth issued for MCC users.
Per-payout authorizationPartnerYou collect consent inside your own product, then ask MCC to execute.
HTG balanceMCCHeld in the MCC ledger. Debited on execution.
Partner daily capMCCuser_connected_apps.daily_limit_htg
User plan capMCCplans.max_withdraw_htg_per_day
Per-payout floor / ceilingMCC1 000 HTG floor · 75 000 HTG network ceiling
MonCash executionMCCVia the MCC executor, then asynchronous delivery confirmation.
Status reporting / webhooksMCCHMAC-signed events to your registered URL
Rewards / fees / commissionsPartnerMCC neither stores nor echoes any reward field.
Sender identityPartnerMCC neither stores nor echoes. Store on your side, indexed on external_reference.

OAuth flow

RFC 6749 authorization-code flow with HTTP Basic client authentication. No refresh tokens. Codes are single-use, valid for 60 seconds; access tokens are valid for 30 days.

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>

The user signs in if they aren't already, sees the consent screen, picks a daily cap, clicks Authorize. The default UX is gateway-only (requires_user_approval = false); the user can opt into a second approval layer, but your application must not depend on it.

On approval → redirect to redirect_uri?code=<raw>&state=<state>. On refusal → redirect to redirect_uri?error=access_denied&state=<state>.

2. Exchange the code for a 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.readscopeRead the user's HTG balance and their plan cap.
payout.requestscopeCreate payouts on behalf of the user — required for external-payout-create.
payout.status.readscopePoll external-payout-status for payouts created on this connection.
Reissuing a token for the same user_connected_app_id automatically revokes the previously active token. Store a single token per connection at a time on the partner side.

Create a payout

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

Canonical body

Nom / CodeType / LimiteDescription
external_reference*string≤ 128 chars. Idempotency key. Unique per user_connected_app_id.
amount_htg*integer1 000 ≤ value ≤ 75 000.
recipient_moncash_number*string8–15 digits. Optional + prefix.
recipient_namestring≤ 200 chars. Shown to the user in the MCC UI.
descriptionstring≤ 280 chars. Shown to the user in the MCC UI.
metadataobjectArbitrary. ≤ 4 KB stringified. Stored as-is on the EPR. Not echoed via webhook.

Tolerant parsing. Any JSON key outside the canonical list above is read and discarded. No validation, no error, no storage, no echo. Compatibility guarantee for partners migrating from v3.

Response

201 Created for a new request, 200 OK on an idempotent replay (matched external_reference) with 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
}

Idempotency — difference vs. /pay-create

Replays are matched on (user_connected_app_id, external_reference). A second POST with the same external_reference returns the current state of the original payout (200 OK, is_replay: true). The replay body is not consulted — any fields you may have modified are ignored. To retry with different fields, generate a new external_reference.

Different convention on the merchant flow /pay-create. That endpoint treats a duplicate referenceId as an error (409 Conflict), not as a replay. Code your retries accordingly.

Read status

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

Requires the payout.status.read scope. Returns the same shape as the partner-view (cf. §webhooks) for any payout previously created on this connection.

Operational reality — asynchronous delivery confirmation

The settlement network for outbound MonCash payouts exposes no functional status GET. The MCC executor submits the withdrawal then waits for delivery confirmation to flip the row. In practice, a payout can sit at submitting for minutes or hours.

This is not a bug. Rely on webhooks for transitions; polling is only useful to reconcile missed events.

Connect rate limits

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

Status vocabulary

Status values on the wire

Nom / CodeType / LimiteDescription
pending_agent_approvalnon-terminalThe user opted into the 2nd approval layer and hasn't acted.
agent_approvednon-terminalUser approved (or auto-approval applies). The executor will take over.
processing_by_moncashconnectnon-terminalMCC is actively executing this payout.
submittingnon-terminalThe settlement network accepted the submission. Delivery confirmation pending — can take hours.
completedterminalDelivery confirmed. The confirmation source is not exposed on the wire.
failedterminalThe executor or the settlement network rejected. Funds not debited or refunded.
rejected_by_agentterminalUser declined on the consent screen.
cancelledterminalCancelled before execution, or connection revoked while the payout was pending.
expiredterminal24h approval window elapsed without user action.

All terminal statuses are stable — once reached, MCC will not change the status again.

Timestamp of completed

The webhook's occurred_at reflects when MCC marked the row as completed, not necessarily when the actual MonCash delivery happened. When confirmation is delayed, the two can be several hours apart. Treat completed as authoritative for state, not as a to-the-second delivery timestamp.

Webhooks

MCC POSTs HMAC-signed events to the URL configured in connected_apps.webhook_url.

Headers sent

Nom / CodeType / LimiteDescription
X-MCC-Event-IduuidUnique event ID — use it for deduplication.
X-MCC-TimestampstringUnix timestamp (seconds). Reject if more than 5 min off.
X-MCC-Signaturestringt=<unix>,v1=<hex(HMAC-SHA256(secret, "<unix>.<body>"))>
Content-Typestringapplication/json
The Connect signature scheme (t=…,v1=…) is different from the merchant flow /pay-create (which signs sha256=<hex> over the raw body, with no X-MCC-Event-Id). If you handle both flows, write two verifiers.

Signature verification

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);

Events

Nom / CodeType / LimiteDescription
external_payout.createdlifecycleEPR inserted.
external_payout.agent_approvedlifecycleStatus moved to agent_approved (or auto_approved at insertion).
external_payout.submitted_to_baziklifecycleThe settlement network accepted the submission; awaiting delivery confirmation. (data.status = 'submitting')
external_payout.completedterminalDelivery confirmed. Source not exposed on the wire.
external_payout.failedterminalExecutor or delivery failure. Funds not debited / refunded.
external_payout.rejectedterminalUser declined on the consent screen.
external_payout.cancelledterminalCancelled, including cancellation via connection revocation.
external_payout.expiredterminal24h approval window elapsed.

Example payload

external_payout.completed
The envelope (event_id, event_type, occurred_at) wraps a partner-view in 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
  }
}

failure_reason vocabulary

When data.status = "failed", this field carries one of:

Nom / CodeType / LimiteDescription
executor_errorMCC-internalExecutor failure before submission to the settlement network. Funds refunded.
delivery_failednetwork/MonCashThe settlement network or MonCash rejected the delivery. Funds refunded.
connection_revokeduserUser revoked the connection during processing. Funds refunded.
insufficient_balanceuserInsufficient MCC balance at execution time (rare — caught at intake).
below_minimumvalidationAmount below the 1 000 HTG floor at execution time.
rejected_by_agentuserUser declined on the consent screen.
failedlegacyUnspecified / legacy. The row is terminal; treat as a soft signal.

Delivery semantics

  • At-least-once. A 2xx from the partner stops retries; otherwise exponential retry up to an attempts cap.
  • Out-of-order delivery is possible. Sort by occurred_at if you track transitions; treat data.status as authoritative.
  • Deduplicate on event_id. Same id = same event, even if the payload differs between attempts.
  • Idempotent reception. Processing the same event_id twice must produce the same result.

Operational expectations

The wire gives you status, not latency. Here's what's normal and what isn't.

Typical happy-path SLA

Nom / CodeType / LimiteDescription
created → agent_approvedinstantAuto-approval applies in the same create request.
agent_approved → submitting0–60sThe executor picks up the queue; the Phase B reconciler covers delays.
submitting → completed0–10 minWhen delivery confirmation arrives quickly (common case).
submitting → completed (long)10 min – several hoursWhen delivery confirmation is delayed; resolved by reconciliation.

What to do if a payout stays at submitting ≥ 30 min

  • Don't cancel, don't retry. The funds are already debited from the user's MCC balance; the payout will be completed (via admin_manual if needed) or refunded.
  • Show "in progress" on the UX, not "failed". The status on the wire stays non-terminal until resolution.
  • Contact MCC support with the external_reference if the latency exceeds your tolerable window. Support will verify independently and confirm the delivery if it did occur.

Error codes

All errors return a JSON object with an error field (machine-stable code) and an error_description field (human-readable).

Nom / CodeType / LimiteDescription
invalid_request400Malformed body, missing canonical field, value out of range. error_description explains.
amount_below_minimum400amount_htg < 1 000.
invalid_token401Bearer missing, token unknown, revoked, or expired.
insufficient_scope403The token lacks the required scope (payout.request, payout.status.read).
connection_revoked403The user revoked the connection on the MCC side after the token was issued.
connection_not_found404The token's user_connected_app_id no longer exists.
partner_daily_cap_exceeded409Your application's per-user daily cap exceeded. DO NOT retry with the same external_reference.
plan_daily_cap_exceeded409The user's plan cap exceeded.
rate_limit_exceeded429Per-IP (240/min), per-app (600/min), or per-connection (30/min).
internal_error500MCC bug. Safe to retry with the same external_reference.
auth_unavailable503Transient infra. Safe to retry.
rate_limit_unavailable503Transient infra. Safe to retry.
{
  "error":             "insufficient_scope",
  "error_description": "Token lacks 'payout.request' scope"
}