Going live: errors, retries, pagination and rate limits
Get a Partner API integration ready for production: handle keys safely, read every error the same way, retry without duplicates, page through lists and stay under the rate limits.
Your integration works. Before real customers depend on it, make sure it handles keys safely, reads errors the same way everywhere, retries without creating duplicates, pages through long lists and backs off when it's asked to.
The Partner API is in early beta. Calls marked available on the API reference are safe to build on, and we won't remove or rename a field on one without telling you first. Response times aren't under an SLA yet.
Keys in production
- One key per integration. Name each one in Settings → API keys in the partner portal so you can tell them apart. An organization can have up to 20 active keys.
- Only the scopes it needs. A reporting job that reads orders doesn't need
orders:write. Scopes never include each other, so the list on a key is exactly what it can do. - Server-side only. Send the key as
Authorization: Bearer …from your backend. Never ship it in a browser or mobile app — the API sends no CORS headers. See Authentication. - Know who it acts as. A key acts on behalf of the admin who created it, so the sales it makes are recorded under that person.
- Plan for expiry. Keys last a year by default. To rotate one, create the new key, deploy it, then revoke the old one.
Call GET/v1/me when your service starts. It needs no scope, and it tells you the key works, which scopes it has and what your account is set up for under entitlements — so a misconfigured key fails at deploy, not halfway through a sale.
curl https://cloud.mergewifi.com/v1/me \
-H "Authorization: Bearer $MERGEWIFI_API_KEY"Read every error the same way
Every error, from every call, has one shape:
{
"error": {
"type": "permission_error",
"code": "missing_scope",
"message": "This API key is missing the `orders:write` scope.",
"request_id": "req_4d00a64235084c1e85dda9c969709906"
}
}- Branch on
code. It's stable.messageis for people and can change. - Fix the field in
param. Validation errors name the field, likepackages[0].plan.plan_id, one problem at a time. - Log
request_id. Every response, success or failure, also carries it in thex-request-idheader. Quote it when you contact us and we can find the exact request.
| Status | type | Codes you'll see | What to do |
|---|---|---|---|
400 | invalid_request_error | invalid_request | Fix the request. Don't retry it unchanged. |
401 | authentication_error | missing_credentials, invalid_credentials, revoked_credentials, expired_credentials | Check the key. A revoked or expired key needs replacing. |
402 | payment_error | payment_failed | The card was declined. Ask for another card. |
403 | permission_error | missing_scope, not_entitled, organization_not_configured, organization_inactive | Add the scope, or contact us about your account. |
404 | not_found_error | resource_not_found | The id is wrong, or not on your account. |
409 | conflict_error | device_unreachable, invalid_state_transition, billing_rejected | The thing you're changing isn't in a state to allow it. Read it again before deciding. |
429 | rate_limit_error | rate_limit_exceeded | Wait for Retry-After, then retry. |
500 | api_error | internal_error | Retry later. If it keeps happening, send us the request_id. |
503 | service_unavailable_error | service_unavailable | Something upstream is briefly unavailable. Wait for Retry-After, then retry. |
More in Errors.
Retry without duplicates
Networks drop requests. Here's what's safe to send again:
| Call | Retry-safe? |
|---|---|
Any GET | Yes. |
| Opening a checkout session | Yes, with the same Idempotency-Key and the same body. You get the original session back. |
| Paying a checkout session | Read the session first. Paying one that's already charged returns it as it is, so a retry can't charge twice. |
| Adding a port forwarding, dedicated IP or bridged IP rule | Yes. An identical rule isn't added twice. |
| Taking a configuration backup | Yes. An extra backup does no harm. |
| Setting or cancelling a restart schedule | Yes. A device has one schedule, and cancelling an empty one succeeds. |
| Device actions: restart, firmware, factory reset, activate, deactivate, tests | No duplicate protection. A retry is sent to the device again, so check the device first. |
Opening a checkout session is the only call that takes an Idempotency-Key today, and it's required there. Use a fresh UUID per sale (up to 64 characters) and store it with the cart before you send. Reusing a key for a different basket is refused rather than handing you the wrong sale. See Retries and duplicates.
Page through long lists
Lists come in three styles. Every one uses the same envelope — data, has_more and next_cursor — so one parser handles them all.
- Cursor pages: orders and devices. Pass
limit(1 to 100, default 25). Whilehas_moreistrue, sendnext_cursorback asstarting_after. Cursors stay correct as new records arrive. - Numbered pages: customers. Pass
pageandlimit, and add one topagewhilehas_moreistrue.next_cursoris alwaysnullhere. - Whole lists: everything else. Products, plans, data plans, network groups, rules and test results come back complete, with
has_moreset tofalse.
A limit outside 1–100 is rejected rather than quietly trimmed, so you never get fewer rows than you think. See Pagination.
Stay under the rate limits
Every response to a valid key tells you where you stand:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Your ceiling for the current window |
X-RateLimit-Remaining | Requests left in it |
X-RateLimit-Reset | When it resets, in Unix seconds |
X-RateLimit-Resource | Which limit these numbers describe: burst, key or org |
By default a key can make 120 requests a minute, no more than 30 of them in any five seconds, and all your organization's keys share a ceiling of 600 a minute. The headers always show the limit you're closest to. Go over and you get a 429 with Retry-After in seconds — wait that long before trying again.
Requests with a bad key are throttled too, by IP address, so a broken key in a retry loop will soon start getting 429s. Fix the key rather than retrying. If your integration needs a higher ceiling, email support@mergewifi.com. See Rate limits.
Launch checklist
- Your key has only the scopes it needs, and lives in your server's secrets, not your code.
- Your service calls GET/v1/me on start-up and fails loudly if a scope is missing.
- You log
request_idon every failure. - Every checkout sends a fresh
Idempotency-Key, and a timed-out payment reads the session before retrying. - Your retries wait for
Retry-Afterand stop on4xxerrors. - Your syncs page with
next_cursor(orpagefor customers) and stop whenhas_moreisfalse. - Someone owns the key's expiry date.
Related: From a new key to your first order · Run your own checkout · Track orders and the devices they ship · API reference

