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, enter your business email and use the verification link you receive. If you already have an account, sign in. Then open Your API keys 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:
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
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.
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.
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
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:
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:
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:
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:
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 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.