# Environments and sandbox

Multigate has two environments. Build and test everything in the **sandbox**, then switch the base URL and credentials to go live.

| | Sandbox | Production |
|---|---|---|
| Base URL | `https://api-gateway-uat.mmacs.io/platform` | `https://api-gateway.mmacs.io/platform` |
| Dashboard | [uat.multigate.app](https://uat.multigate.app) | [multigate.app](https://multigate.app) |
| Money | Simulated. Nothing reaches a real bank account. | Real. |
| Credentials | Sandbox API key and client secret (from the sandbox dashboard) | Production API key and client secret (from the production dashboard) |
| IP allow-list | Its own list, enforced | Its own list, enforced |
| Webhook URL | Registered in the sandbox | Registered in production |
| Funding your accounts | [Simulate an inflow](#simulate-an-inflow) | Real bank transfers to your virtual accounts |

Every path in these docs is relative to the base URL. For example, getting the current user in the sandbox is:

```text
GET https://api-gateway-uat.mmacs.io/platform/user-service/api/v1/users/current-user
mp-api-key: <your sandbox API key>
```

A key works only in the environment whose dashboard generated it, and its start tells you which one: sandbox keys start with `mgk_sandbox_` and production keys with `mgk_live_`.

:::tip[Keep the base URL in configuration]
Read the base URL and credentials from configuration, for example the environment variables `MG_BASE_URL`, `MG_API_KEY` and `MG_CLIENT_SECRET` used throughout these docs (set up in the [Quickstart](03-quickstart.md#before-you-start)). Going live is then a configuration change, not a code change.
:::

## How the sandbox differs from production

:::danger[Read this before you test]
The sandbox doesn't behave exactly like production. A few checks are **off** (FX balance checks, NGN beneficiary account checks, disposable email domains), some jobs are much **slower** (payout results, customer reviews), and payout outcomes come from **test banks**, sometimes at random.

Every difference, with what to do about it, is on [Sandbox vs production](05-sandbox-vs-production.md).
:::

| | Sandbox | Production |
|---|---|---|
| Incoming payments | Only through [Simulate an inflow](api:simulateInflow). | From payers' banks. The simulate endpoint does nothing. |
| Payouts | Full flow (validation, screening, approvals) against test banks and simulated partners. Results can take 15 minutes or more. | Paid to real accounts, usually within seconds of the bank answering. |
| Checks that are off | FX balance check on lock, NGN account check when saving a beneficiary, disposable email domains. | All on. |
| Webhooks and emails | Sent, with the same format and signing. Emails go to real addresses. | Sent. |
| IP allow-list | Enforced (its own list). | Enforced (its own list). |
| Data | Separate from production. Nothing carries over when you go live. | |

## Simulate an inflow

Fund a sandbox account, or test your inflow handling, by pretending a payer sent money to one of your virtual accounts.

<Steps>
<Step title="Find an active account number">

Use [List accounts](api:listAccounts) and pick an account whose `status` is `active`. Use one of its virtual account numbers from `fundingDetails` (the numbers payers would send to). The account must belong to your business or one of your active customers. The [Quickstart](03-quickstart.md) stores it in `MG_VIRTUAL_ACCOUNT_NUMBER`.

</Step>
<Step title="Send the simulated inflow">

```bash
curl -s -X POST "$MG_BASE_URL/client-service/api/v1/webHook/publishInflow" \
  -H "mp-api-key: $MG_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"accountNumber\":\"$MG_VIRTUAL_ACCOUNT_NUMBER\",\"amount\":1000000,\"currency\":\"NGN\"}"
```

```json
{ "message": "inflow initiated" }
```

`amount` is in major units (`1000000` is 1,000,000.00 NGN). `currency` is required but ignored: the credit always uses the account's own currency. The response contains no reference.

</Step>
<Step title="Watch it arrive">

The response only means the inflow was queued. It is then screened (checked against Multigate's compliance rules) and credited like a real one. In the sandbox this usually takes a few minutes; there is no fixed time. Then:

- your webhook receives `account` / `inflow`;
- your business contact receives a "Payment Received" email;
- the balance ([Get an account balance](api:getAccountBalance)) and statement of the account show the credit, minus any collection fee (a fee on money received) configured for your business.

A simulated inflow has no sender bank or sender account number (`null` in the webhook), and its `paymentReference`/`sessionId` is a generated ID the call doesn't return. The sender name is the account's own name, and the narration looks like `MULTIGATE-<account name>-<masked account number>`.

</Step>
</Steps>

:::warning[Check the response body, not only the status]
If the account isn't active yet, the endpoint still answers `200`, but with an error body: `[{"errors":{"message":"account has not been approved"}}]`. Wait for the `account_opened` webhook before simulating inflows into a new account.
:::

:::caution[Check the credited amount]
For some NGN and GHS sandbox accounts, depending on the partner bank behind them, the simulated amount is read as minor units (kobo or pesewas) and credited at one hundredth: `1000000` credits 10,000.00. Compare `amount` in the webhook with what you sent, and send 100 times the amount if needed.
:::

In production the endpoint answers `200` but does nothing, so keep it out of your production code paths entirely.

## Test checklist

Before you ask for production access, run each of these in the sandbox:

- [ ] Call the API with your sandbox API key, [rotate it](06-authentication.md#rotate-an-api-key) to a second key without errors, and handle a `401` from a revoked key ([Authentication](06-authentication.md#when-a-request-returns-401)).
- [ ] Fund an account with a simulated inflow and process the `inflow` webhook ([Collections](../04-products/03-collections.md)).
- [ ] Create a payout, and process `transfer_completed` and `transfer_failed` ([Payouts](../04-products/05-payouts.md)).
- [ ] Retry a payout with the same `uniqueRequestId` and confirm nothing is paid twice ([Idempotency](../02-concepts/03-idempotency.md)).
- [ ] Trigger and handle errors: insufficient balance, invalid account, unknown bank ([Errors](../02-concepts/02-errors.md)).
- [ ] Reject a webhook with a tampered body ([Verifying signatures](../03-webhooks-and-emails/03-verifying-signatures.md)).

Then read [Sandbox vs production](05-sandbox-vs-production.md) and work through the [go-live checklist](07-go-live-checklist.md).
