# Test your Graph integration in the sandbox This guide tests the Mailionaire API workflow for your own Microsoft Graph app. It creates **simulated** orders, tenant IDs and mailbox UPNs. It does not contact Microsoft, grant consent, issue OAuth tokens, send email, change DNS or charge you. Real Graph orders and Microsoft execution are currently disabled. ## 1. Get your first key Open [Mailionaire signup](https://mailionaire.ch/whitelisted), enter your business email and use the verification link you receive. If you already have an account, sign in. Then open [Your API keys](https://app.mailionaire.ch/portal/api#keys-panel) directly. You do **not** need to finish registrar/sequencer onboarding, connect any provider, add a payment method or order paid mailboxes for this test. Select **Manage my Microsoft Graph integration**, create a key and copy it once. The initial key starts with `mk_live_`. A normal key without this selection cannot manage Graph profiles. If the option is missing, contact support; do not order mailboxes to unlock it. The following examples use **Bash**, `curl` and `jq`. Open Bash before copying the commands, then keep using the same terminal. Enter the initial key privately: ```bash BASE='https://api.mailionaire.ch/v1' read -r -s -p 'Initial mk_live_ key: ' LIVE_KEY printf '\n' ``` Never paste keys into tickets or shared transcripts. Use the initial key only to create the sandbox key below; never use it for a test order. ## 2. Create and verify a sandbox key ```bash KEY_RESPONSE=$(curl --fail-with-body -sS "$BASE/keys" \ -H "Authorization: Bearer $LIVE_KEY" -H 'Content-Type: application/json' \ --data '{"label":"Graph sandbox","environment":"sandbox","scopes":["account:read","domains:read","domains:write","mailboxes:read","mailboxes:write","orders:read","orders:write","webhooks:read","webhooks:write","keys:read","keys:write","graph:read","graph:write"]}') TEST_KEY=$(printf '%s' "$KEY_RESPONSE" | jq -er '.api_key') TEST_KEY_ID=$(printf '%s' "$KEY_RESPONSE" | jq -er '.key_id') unset KEY_RESPONSE [[ "$TEST_KEY" == mk_test_* ]] || { echo 'STOP: expected a sandbox key'; exit 1; } curl --fail-with-body -sS "$BASE/me" \ -H "Authorization: Bearer $TEST_KEY" | jq -e '.environment == "sandbox"' ``` Expected: `true`. **Stop if any command fails.** Use only `TEST_KEY` below. The explicit scopes include `graph:read` and `graph:write`. Omitting `scopes` inherits the initial key's permissions; it cannot add permissions the parent key does not have. ## 3. Choose a test domain and register a synthetic app Use a returned `.test` domain. Do not substitute a real customer domain. Your real Entra app ID or credentials are **not needed** for this simulation. ```bash DOMAIN=$(curl --fail-with-body -sS "$BASE/domains/available" \ -H "Authorization: Bearer $TEST_KEY" | jq -er '.items[0].domain') [[ "$DOMAIN" == *.test ]] || { echo 'STOP: expected a .test domain'; exit 1; } PROFILE=$(curl --fail-with-body -sS "$BASE/graph/profiles" \ -H "Authorization: Bearer $TEST_KEY" -H 'Content-Type: application/json' \ --data '{"label":"Synthetic sandbox app","app_id":"42a1d760-3dfe-4cce-89db-ccf6131cd539","permissions":["Mail.Send"],"scope_mode":"provisioned_mailboxes"}') PROFILE_ID=$(printf '%s' "$PROFILE" | jq -er '.id') curl --fail-with-body -sS -X PUT "$BASE/domains/$DOMAIN/graph-integration" \ -H "Authorization: Bearer $TEST_KEY" -H 'Content-Type: application/json' \ --data "$(jq -nc --arg id "$PROFILE_ID" '{profile_id:$id}')" | jq curl --fail-with-body -sS -X PUT \ "$BASE/domains/$DOMAIN/graph-integration/provisioning-destination" \ -H "Authorization: Bearer $TEST_KEY" | jq ``` The example app ID is synthetic. Never consent to it in a real tenant. The binding and destination record the simulated provisioning choice, not Microsoft access. ## 4. Optional: receive signed lifecycle webhooks Skip this step if you only want to poll the order. To test delivery, first provide your own **public HTTPS receiver** that promptly returns a 2xx response. Replace the example URL; Mailionaire does not host your receiver. ```bash WEBHOOK_URL='https://your-platform.example/hooks/mailionaire' HOOK=$(curl --fail-with-body -sS "$BASE/webhooks" \ -H "Authorization: Bearer $TEST_KEY" -H 'Content-Type: application/json' \ --data "$(jq -nc --arg url "$WEBHOOK_URL" \ '{url:$url,events:["order.created","order.completed","domain.ready"]}')") WEBHOOK_ID=$(printf '%s' "$HOOK" | jq -er '.id') WEBHOOK_SECRET=$(printf '%s' "$HOOK" | jq -er '.secret') unset HOOK curl --fail-with-body -sS -X POST "$BASE/webhooks/$WEBHOOK_ID/test" \ -H "Authorization: Bearer $TEST_KEY" | jq -e '.delivered == true' ``` Expected: `true` and a `ping` at your receiver. Keep `WEBHOOK_SECRET` private; it is shown once. Check `X-Mailionaire-Signature` against `sha256=HMAC-SHA256(secret, timestamp + "." + raw_request_body)`, using the `X-Mailionaire-Timestamp` header and the exact original body bytes. Compare in constant time, reject stale timestamps and deduplicate by event ID. The signing secret is not an API key. Fix a failed ping before continuing if testing webhooks. ## 5. Create a simulated Graph order ```bash IDEMPOTENCY_KEY="graph-sandbox-$(date +%s)" ORDER_BODY=$(jq -nc --arg d "$DOMAIN" --arg p "$PROFILE_ID" --arg i "$IDEMPOTENCY_KEY" \ '{domain:$d,mailbox_count:2,name_mode:"automatic",idempotency_key:$i,sequencer_provider:"customer_graph",graph_profile_id:$p}') ORDER=$(curl --fail-with-body -sS "$BASE/orders" \ -H "Authorization: Bearer $TEST_KEY" -H 'Content-Type: application/json' \ --data "$ORDER_BODY") ORDER_ID=$(printf '%s' "$ORDER" | jq -er '.order_id') printf '%s\n' "$ORDER" | jq ``` Expected: HTTP 201 and an `order_id`. No billing confirmation is needed. If a response is uncertain, retry the same request with the **same `ORDER_BODY`**; it returns the same order. Do not generate a new idempotency key for that retry. Poll every few seconds until `status` is `completed`: ```bash curl --fail-with-body -sS "$BASE/orders/$ORDER_ID" \ -H "Authorization: Bearer $TEST_KEY" | jq ``` Orders progress through queued, prepared, running and completed; short intermediate states may be missed. A failed order is not a completed test. If it fails or stays pending for several minutes, inspect `GET /orders/{order_id}/logs` and contact support with the order ID and error code, never your keys. If you registered a webhook, expect `order.created`, `order.completed` and `domain.ready`. Inspect delivery status with: ```bash curl --fail-with-body -sS "$BASE/webhooks/deliveries" \ -H "Authorization: Bearer $TEST_KEY" | jq ``` ## 6. Refresh the simulated tenant and mailbox inventory Run this **after the order completes**: ```bash curl --fail-with-body -sS -X POST "$BASE/domains/$DOMAIN/graph-integration/refresh" \ -H "Authorization: Bearer $TEST_KEY" | jq curl --fail-with-body -sS "$BASE/domains/$DOMAIN/graph-integration" \ -H "Authorization: Bearer $TEST_KEY" | jq ``` Success means `state: "simulated"`, a synthetic `tenant_id`, two mailbox UPNs and object IDs, and **`scope_verified: false`**. That false value is expected: no Microsoft access has been granted or verified. Empty inventory before order completion is pending. The sandbox IDs/UPNs cannot be used to obtain tokens, log into Microsoft, send email or measure deliverability. ## 7. Clean up If you created a webhook, remove it first. A successful deletion returns HTTP 204 with no response body: ```bash if [[ -n "${WEBHOOK_ID:-}" ]]; then curl --fail-with-body -sS -X DELETE "$BASE/webhooks/$WEBHOOK_ID" \ -H "Authorization: Bearer $TEST_KEY" fi curl --fail-with-body -sS -X DELETE "$BASE/domains/$DOMAIN/graph-integration" \ -H "Authorization: Bearer $TEST_KEY" | jq curl --fail-with-body -sS -X DELETE "$BASE/graph/profiles/$PROFILE_ID" \ -H "Authorization: Bearer $TEST_KEY" | jq curl --fail-with-body -sS -X DELETE "$BASE/keys/$TEST_KEY_ID" \ -H "Authorization: Bearer $TEST_KEY" | jq unset LIVE_KEY TEST_KEY WEBHOOK_SECRET ``` Expected binding state: `revoked`. This retires the profile and revokes the test key while retaining audit history. Revoke the initial key in [Your API keys](https://app.mailionaire.ch/portal/api#keys-panel) if no longer needed. For another clean run, create a fresh synthetic app ID and use an unused test domain; a revoked binding cannot be reused. ## Troubleshooting - **401:** key missing, revoked or copied incorrectly. Create a replacement securely. - **403 / insufficient scope:** create the initial key with the Graph checkbox, then a sandbox child with the scopes above. Existing normal keys cannot upgrade themselves. - **Graph preview unavailable:** contact support; no payment or live order will fix this. - **409 / binding conflict:** use an unused test domain and a new synthetic app ID. - **Webhook ping fails:** check your public HTTPS URL, TLS, receiver logs and 2xx response. You can still test orders and inventory by polling. - **Inventory is empty:** wait for order completion, then call refresh again. ## Before a real Microsoft pilot We still need your Application/Client ID, app home Tenant ID, confirmation that you control the multitenant app, exact requested application permissions, and whether you use a client secret or certificate. **Do not send secrets or private keys.** We must separately agree on the test tenant/domain and mailbox scope, then validate actual authorization and access before enabling real Graph orders. A successful sandbox run is not proof of real Microsoft access. [API reference](https://api.mailionaire.ch/v1/docs) ยท [OpenAPI schema](https://api.mailionaire.ch/v1/openapi.json)