Sandbox Quickstart

Sandbox mode is a free test environment baked into MonCashConnect. Build your integration, run end-to-end payment flows, see webhooks fire — all without moving real money or touching real MonCash. When you're ready, swap two secret values and you're live. Same API, same payloads, same response shapes.

This guide takes you from signup to your first live payment in under 30 minutes. No code change between sandbox and live — only two environment variables flip.

Before you start

You need:

  • A MonCashConnect account at moncashconnect.com — no identity verification needed to test in Sandbox. KYC is only required to activate Live payments.
  • A backend that can make outbound HTTPS requests and host a webhook endpoint.
  • A domain where customers will be redirected after payment.

Implement with AI

Most merchants already have an AI coding assistant in their workflow — Lovable, Cursor, Claude, ChatGPT, Copilot. The prompt below encapsulates the entire MCC sandbox integration as a single paste: your assistant will ask you three questions, then write the backend, the webhook, the checkout button and the return page.

Before pasting: have your MCC test secret + your test webhook secret ready (from "Régénérer les clés de test" on the Projects tab of the Developer page) plus the domain of your site.

You are implementing MonCashConnect (MCC) sandbox payments on this project.

MCC is a Stripe-style payment gateway for MonCash (Haiti's mobile money). Sandbox mode lets us test the full payment flow without moving any real money. When we're ready, the same code goes live by swapping two env values — no code change required.

The user will provide three pieces of info. Ask for them before writing code. Do not invent placeholders.

- DOMAIN: the merchant's domain (e.g. mamasoil.shop). It becomes the Origin header on the call to MCC, and must already be in MCC's "Allowed domains" list (the user adds it in the MCC dashboard).
- MCC_SECRET: starts with `sk_test_proj_`. From MCC dashboard → Developer → Projets → "Régénérer les clés de test" modal.
- MCC_WEBHOOK_SECRET: the test webhook secret from the same modal.

== MCC API contract ==

1) Create a payment — your backend → MCC
   POST https://hvlmeoqyxaguzcujpmit.supabase.co/functions/v1/pay-create
   Headers:
     Authorization: Bearer <MCC_SECRET>
     Origin: https://<DOMAIN>      ← MUST match Allowed domains exactly. Server-side fetch does not set Origin automatically; set it manually.
     Content-Type: application/json
   Body:
     {
       "amount": <integer HTG, 1 to 1_000_000>,
       "referenceId": "<unique id you generate, UUID is fine>",
       "returnUrl": "https://<DOMAIN>/checkout/return",
       "customerEmail": "optional@example.com",   ← OMIT (not empty string) if absent
       "customerName": "optional"                  ← OMIT if absent
     }
   Response 200:
     { paymentUrl, reference, expiresAt, livemode: false }
   Redirect the browser to paymentUrl. In sandbox it's the MCC simulator (3-button page); in live it's the real MonCash URL.

2) Receive webhook — MCC → your backend
   POST <your webhook endpoint, the URL the user will set in MCC project settings>
   Headers:
     X-MCC-Signature: sha256=<hex>
     X-MCC-Timestamp: <unix seconds>
     Content-Type: application/json
   Body:
     {
       event: "payment.completed" | "payment.failed" | "payment.cancelled",
       reference: "<the referenceId you sent earlier>",
       amount: <integer HTG>,
       status: "completed" | "failed" | "cancelled",
       completedAt: <ISO8601 or null>,
       livemode: false              ← present on sandbox only; ABSENT on live (do NOT gate with === true)
     }

   Verification (CRITICAL):
     a. Read the body as RAW BYTES first (e.g. req.text() in Deno, raw req in Express). Do NOT parse JSON before verifying — re-serializing reorders keys and breaks HMAC.
     b. Compute expected = "sha256=" + hex(HMAC-SHA256(MCC_WEBHOOK_SECRET, rawBody))
     c. Compare to X-MCC-Signature header. Reject with 401 if mismatch.
     d. Only AFTER verifying, JSON.parse(rawBody) and dispatch on event.
   Handle ALL three event types. Returning non-200 on any of them causes MCC to retry forever.
   Return 200 with any short body on success.

3) Poll status (webhook fallback) — your return page → MCC, no auth
   GET https://hvlmeoqyxaguzcujpmit.supabase.co/functions/v1/pay-status?reference=<referenceId>
   Response: { reference, status, amount, ... }
   Use this from the customer's /checkout/return page, polling every 3 seconds for up to 90 seconds, in case the webhook is delayed.

== Files to create ==

1. Backend endpoint `create-mcc-payment` (or your stack's equivalent)
   Calls MCC pay-create with MCC_SECRET. Must handle CORS preflight (OPTIONS → 204 with CORS headers).

2. Backend endpoint `mcc-webhook`
   Receives, verifies HMAC, dispatches on event. Uses raw body for HMAC, parses JSON only after verifying.

3. Frontend "Pay with MonCash" button on the checkout page
   Calls create-mcc-payment with the amount, then window.location = paymentUrl.

4. Frontend `/checkout/return` page
   Reads ?reference, ?error, ?cancelled from the query string. Polls pay-status every 3s up to 30 attempts (90s total) as the webhook fallback. Shows success / failed / cancelled / timeout state to the customer.

5. Two environment variables on the backend
   MCC_SECRET           = sk_test_proj_<...>      (sandbox value for now)
   MCC_WEBHOOK_SECRET   = <test webhook secret>
   Use GENERIC names (no _TEST_) so the live swap is a value change, not a code change.

6. The MCC project's "Webhook URL" field (the user sets this in MCC dashboard) must point at your deployed mcc-webhook endpoint.

== Critical gotchas ==

- Origin header MUST be set on the server-side fetch to MCC. Browsers add it automatically; servers do not.
- Raw body for HMAC verify, never the parsed-then-restringified payload.
- All three webhook event types (completed / failed / cancelled) must be handled — non-200 = infinite retry.
- `livemode` is `false` on sandbox payloads and ABSENT on live. Never gate logic with `livemode === true`.
- CORS preflight on create-mcc-payment — return CORS headers on OPTIONS, not just on POST.
- Omit customerEmail / customerName if absent; do not send empty string.
- amount is integer HTG between 1 and 1,000,000. Recommended test value: 100.

== Out of scope (do NOT do these) ==

- Live mode. Implement sandbox only. Live is one env swap later, not a code change.
- The merchant's own order persistence / inventory / fulfillment logic. Hand them the payment.completed event and let them wire it.
- Translating "Sandbox" to French if generating French copy. It's the product feature name and stays English (like Stripe).

== When done ==

Give the user:
1. The two env vars they need to set + the webhook URL they need to paste into the MCC project.
2. A 4-step test plan: open the simulator URL returned from pay-create, click Pay success on the MCC simulator, verify webhook arrives in your logs + your /checkout/return resolves to "completed".
3. A one-line note: going live = swap MCC_SECRET to sk_proj_<...> + MCC_WEBHOOK_SECRET to the live webhook secret, no code change.

Lovable — paste it in the chat as-is.

Claude / ChatGPT — same paste; the assistant will ask you for the three values.

Cursor / Copilot — open a chat panel in your repo, paste, answer the three questions when prompted.

What you'll need to provide

  • Your domain (e.g. mamasoil.shop)
  • Your MCC_SECRET (starts with sk_test_proj_)
  • Your MCC_WEBHOOK_SECRET (the test webhook secret)

After the AI implements and you deploy, come back to the next section to verify the API contract matches what was built.

1. Create a project

Sign in → DeveloperProjets Nouveau projet.

Fill in

Nom / CodeType / LimiteDescription
NameinternalFor your reference — never shown to customers.
Webhook URLHTTPS requiredWhere MCC will POST events.
Allowed domainsexactExact domains your backend will declare as Origin when calling MCC's API. Mismatch = 403.

On submit, you'll see a one-time modal with FOUR values. Save them all:

Nom / CodeType / LimiteDescription
Live publishablepk_proj_<32 hex>Optional — client-side rendering.
Live secretsk_proj_<48 hex>Bearer token for live payments.
Live webhook secretwhsec_<48 hex>HMAC key for live webhook verification.
Test secretsk_test_proj_<48 hex>Bearer token for sandbox.
Save these immediately. Full secrets are shown ONCE. Lost = "Régénérer les clés" invalidates the previous one.

The test publishable + test webhook secret are also surfaced on the Projects tab (the test webhook secret via "Régénérer les clés de test").

2. The MCC API

Three endpoints. Same for sandbox and live — only the bearer token changes.

2.1 Create a payment

POST
https://hvlmeoqyxaguzcujpmit.supabase.co/functions/v1/pay-create

Headers:

  • Authorization: Bearer <YOUR_SECRET>
  • Origin: https://YOUR-ALLOWED-DOMAIN
  • Content-Type: application/json

Body:

{
  "amount": 100,
  "referenceId": "<your-unique-id>",
  "returnUrl": "https://YOUR-DOMAIN/checkout/return",
  "customerEmail": "optional@example.com",
  "customerName": "optional"
}

Response (200):

{
  "paymentUrl": "https://moncashconnect.com/sim/<reference>?p=<short>",
  "reference": "<your-unique-id>",
  "expiresAt": "2026-01-01T00:00:00Z",
  "livemode": false
}
Constraints
  • amount: integer HTG, 1 to 1,000,000.
  • referenceId: your unique id (UUID recommended), 100 chars max, [a-zA-Z0-9\-_].
  • returnUrl: must be HTTPS.
  • Origin header is REQUIRED and must match Allowed domains.
  • Omit customerEmail / customerName if absent — empty strings trip validation.

Redirect the customer's browser to the returned paymentUrl. In sandbox: the MCC simulator. In live: the real MonCash page.

2.2 Receive webhooks

MCC POSTs to your webhook URL on every status change. Verify the HMAC-SHA256 signature before trusting the payload.

Headers:

  • Content-Type: application/json
  • X-MCC-Signature: sha256=<hex>
  • X-MCC-Timestamp: <unix seconds>

Body:

{
  "event": "payment.completed" | "payment.failed" | "payment.cancelled",
  "reference": "<your-unique-id>",
  "amount": 100,
  "status": "completed" | "failed" | "cancelled",
  "completedAt": "2026-01-01T00:00:00Z" | null,
  "livemode": false
}
Verification
  1. Read the request body as RAW BYTES (do not parse JSON first — re-serializing breaks HMAC).
  2. Compute sha256=<hex> of HMAC-SHA256(your-webhook-secret, raw-body).
  3. Compare against the X-MCC-Signature header.
  4. If match, parse the JSON and dispatch on event.
Handle ALL three events. A non-200 response on ANY event causes MCC to retry indefinitely.
livemode is false on sandbox payloads and ABSENT on live payloads. Don't gate logic with livemode === true — it will never be true.

2.3 Poll payment status (fallback)

GET
https://hvlmeoqyxaguzcujpmit.supabase.co/functions/v1/pay-status?reference=<your-unique-id>

No auth required. Returns the same payment record. Useful for your return page when the webhook hasn't arrived yet.

3. Test in sandbox

3.1 Set up your environment

Two secrets in your backend's .env (or equivalent):

MCC_SECRET = sk_test_proj_<your-test-secret>
MCC_WEBHOOK_SECRET = <your-test-webhook-secret>
Use generic variable names (MCC_SECRET, not MCC_TEST_SECRET). When you go live, you swap values — no code change.

3.2 Trigger a sandbox payment

Recommended test amount: 100 HTG (~$0.75 USD). Same amount you'll use for the live verification later — small enough to keep both tests cheap, large enough to behave like a real payment.

From your checkout, call your pay-create endpoint. The response gives you a paymentUrl. Redirect the browser there.

3.3 What you'll see on the simulator

The browser opens https://moncashconnect.com/sim/<reference>?p=<short>. A centered card with:

  • An amber SANDBOX badge
  • "Test transaction — no real money"
  • Your reference id
  • Three buttons:
    • Pay success (green) — flips status to completed, credits the sandbox ledger.
    • Pay fail (red) — flips status to failed, no credit.
    • Cancel / timeout (outline) — flips status to cancelled, no credit.
  • An explainer footer

Click any of them.

3.4 What happens

  1. Transaction status flips in MCC's database.
  2. Webhook fires to your endpoint with livemode: false and the matching event.
  3. Browser redirects to <your returnUrl>?reference=<id> (+ &error=true&cancelled=true for cancel, &error=true for fail).

3.5 Where to verify in MCC

After clicking a simulator button:

1

MCC Dashboard → /transactions

Pill toggle at the top: En direct | Test. Click Test. Your sandbox payments appear with status, amount, in/out coloring, and reference. Sandbox txs are NEVER mixed with live; they're isolated.

2

Your webhook handler logs

One POST should be visible with a valid HMAC signature and the matching event type.

3

Your return page

Should reflect the outcome.

All three in sync → integration validated.

3.6 Run all three outcomes

Nom / CodeType / LimiteDescription
Pay successpayment.completed?reference=<id>
Pay failpayment.failed?reference=<id>&error=true
Cancelpayment.cancelled?reference=<id>&error=true&cancelled=true

Trigger one fresh payment per outcome (each transaction can only be transitioned once).

4. Going live

After all three sandbox outcomes verify cleanly:

4.1 Swap two secret values

Nom / CodeType / LimiteDescription
MCC_SECRETsk_test_proj_…Live: sk_proj_…
MCC_WEBHOOK_SECRETtest webhook secretLive: live webhook secret
No code changes. API contract is identical.

4.2 What changes in live mode

Nom / CodeType / LimiteDescription
paymentUrlSandbox: MCC simulatorLive: real MonCash URL (Digicel-hosted)
Customer flowSandbox: 3 buttonsLive: real MonCash page: phone, PIN, confirm
SettlementSandbox: instantLive: ~5-30 seconds after PIN; SMS confirmation
livemode (webhook)Sandbox: falseLive: absent
MoneySandbox: noneLive: real HTG, ~3% commission
Dashboard tabSandbox: TestLive: En direct (default)

4.3 First live test

Recommended: the same 100 HTG amount you tested in sandbox.

  1. Click your checkout button. Browser opens a real MonCash URL.
  2. Enter your real MonCash phone number + PIN. Confirm.
  3. Browser redirects to your return URL with ?reference=…
  4. Within 30-60 seconds, MCC's reconciler updates the transaction to completed and fires the webhook.
  5. Verify in MCC /transactions (En direct tab) — your 100 HTG row, completed, ~3 HTG commission, ~97 HTG net.

4.4 Webhook delivery caveat

There's a known issue where live webhooks may arrive late or with signatures that don't verify on first try. Mitigation: implement a pay-status poll on your return page for up to 90 seconds. Your customer sees the confirmation immediately regardless of webhook timing.

5. Common errors

Nom / CodeType / LimiteDescription
403 Origin not allowedOrigin header ≠ Allowed domainsAdd domain to MCC project.
project_lookup_failed 502Wrong bearer tokenRe-copy from MCC dashboard.
tx_not_found_or_not_pendingTx already processedTrigger fresh payment.
bad signature (webhook)Used parsed JSON for HMACUse raw body bytes.
Sandbox webhook OK, live doesn'tKnown issue, see §4.4Use pay-status polling fallback.
Browser console: Failed to fetchCORS preflight on YOUR backendReturn CORS headers on OPTIONS.

FAQ

Does sandbox cost anything?

No.

Can I skip KYC for sandbox?

Yes. Sandbox is open to any signed-in account — create a project, grab your sk_test_proj_… keys and start integrating right away. Identity verification (KYC) is only required to activate Live mode (sk_proj_…).

Are sandbox transactions visible to MCC admins?

No. Merchant's /transactions (Test tab) only. Never affects dashboard summaries, finance, or admin views.

Multiple projects?

Yes. Each gets its own live + test key pair + webhook URL.

Min / max amount?

1 to 1,000,000 HTG.

Does sandbox simulate MonCash's UI?

No — three buttons. Sandbox tests your integration, not Digicel's UI.

Webhook signature scheme?

HMAC-SHA256, X-MCC-Signature: sha256=<hex>. Same in sandbox and live.

Going live = redeploy?

No. Only environment variable values change.

Refunds?

Not exposed via public API yet. Contact MCC support.

Status

This guide is published as a public reference. If anything is unclear, contact MCC support with your project name, the function log snippet, and the reference UUID.