Errors
Every failure has the same shape, and it never contains a provider’s prose or your data:
{ "ok": false, "code": "api_key_scope_denied" }code is the machine-readable part; the message is written to MauKirim’s log and never returned, so you
can log a response body without worrying that it carries a phone number.
Auth and access
Section titled “Auth and access”| HTTP | code |
Condition |
|---|---|---|
| 401 | api_key_invalid |
No Authorization header, not Bearer <token>, or no key matches |
| 401 | api_key_revoked |
The key was revoked |
| 401 | api_key_expired |
The key’s expiry has passed |
| 403 | api_key_scope_denied |
The key lacks the scope this route requires |
| 403 | forbidden |
The key is fine but the account is missing or not active |
| 404 | rental_not_found |
The device is not rented by this account — also returned for a device that does not exist |
Request shape
Section titled “Request shape”| HTTP | code |
Condition |
|---|---|---|
| 400 | invalid_input |
Missing or malformed field; wrong Content-Type (three routes require application/json); body larger than 8 KB; missing Idempotency-Key; template scalar or item-list variables that do not match the reviewed placeholders |
| 409 | batch_idempotency_conflict |
The Idempotency-Key was already used with a different request |
| 413 | batch_too_large |
Text over 4096 characters, caption over 1024, or an Idempotency-Key over 128 |
| 429 | rate_limited |
Too many OTP sends for the same number — see below. (Google Business: also too many connect sessions in an hour) |
Sending
Section titled “Sending”| HTTP | code |
Condition |
|---|---|---|
| 402 | insufficient_credits |
Credit balance is below the cost of this message |
| 409 | device_not_connected |
No MauKirim sending number is ready right now |
| 503 | worker_offline |
The dispatch worker has no fresh heartbeat — transient, retry |
| 409 | batch_idempotency_conflict |
See above: same key, different request |
| 404 | gateway_not_found |
The device’s gateway no longer exists |
| 409 | gateway_disabled |
The device’s gateway is switched off by an operator |
| 410 | media_unavailable |
The media id is unknown, owned by another account, expired (7 days), or no bucket is configured |
| 404 | device_not_found |
The device id is unknown to this deployment |
| 409 | rental_device_not_ready |
The device is reachable but too stale to accept the operation |
| 400 | invalid_input |
Image not JPEG/PNG, empty, over 5 MiB, or its bytes do not match the declared type |
Webhooks
Section titled “Webhooks”| HTTP | code |
Condition |
|---|---|---|
| 404 | webhook_not_configured |
Rotation or deletion on a device with no destination |
| 400 | invalid_input |
Destination URL missing, not https, carrying credentials/query/fragment, an IP literal or localhost; unknown event name |
Google Business
Section titled “Google Business”The connector’s codes. All of them keep the standard envelope; gmb_rate_limited alone carries one more
field.
| HTTP | code |
Condition |
|---|---|---|
| 402 | gmb_plan_required |
The account’s package does not include the Google Business connector (Growth and above do). Upgrade, or ask an operator to add it |
| 404 | gmb_location_not_found |
The business does not exist, is not on this account, or has left Google (removed_upstream). Identical for all three — the API never confirms that a business id is real |
| 404 | gmb_connection_not_found |
The connection does not exist or is not this account’s |
| 429 | gmb_rate_limited |
No Google budget right now: the shared pool of 300 queries a minute, this account’s own share, or a short pause after Google itself answered 429. Carries Retry-After and retryAfterSeconds |
| 409 | gmb_reconnect_required |
Google no longer accepts the saved permission (revoked, expired or password changed). The account owner must reconnect in the dashboard |
| 403 | gmb_forbidden |
Google refused: the connected Google account is probably not an Owner or Manager of that business, or the permission was narrowed |
| 503 | gmb_api_unavailable |
Google refused the request because a Business Profile API is not enabled or not approved for the MauKirim deployment’s Google project: the core API (then everything fails, including the first sync) or one feature’s own API (insights, verification, place action links, lodging). Not an account problem; MauKirim support fixes it |
| 409 | gmb_location_disabled |
The business or its connection is switched off: by an operator, because the connection was disconnected, or by you (disabledBy: user: every business found on connect or sync starts off until you turn it on). From the activation endpoint it also means MauKirim support switched the business off, and it cannot be turned on there |
| 409 | gmb_location_limit |
The package’s allowance of active businesses is full, so no more can be turned on or created (10 on Growth, 50 on Business, 200 on Scale, 1,000 on Platform); businesses that are switched off do not count. Turning businesses on is atomic: when it would not fit nothing changes. A create is refused with this before anything exists on Google |
| 409 | gmb_idempotency_conflict |
An Idempotency-Key was reused with a different edit, reply, upload, invitation or new business |
| 413 | gmb_file_too_large |
A photo upload is over 5 MB. Refused from Content-Length before the body is read, and nothing is sent to Google |
| 415 | gmb_unsupported_media |
A photo upload is not a JPEG or PNG. The file’s bytes decide, not its name or declared type. Convert it and send it again |
| 400 | gmb_authorization_failed |
The Google consent round trip failed — denied, expired, or no refresh permission granted. Shown in the dashboard, and as the error of a failed connect session |
| 400 | gmb_redirect_not_allowed |
The redirectUrl of a connect session, or a return address being registered, is not an https address (http only on localhost), has credentials, a fragment (or, when registering, a query), is not absolute or is too long — or it is not inside one of the account’s registered return addresses. Nothing is created |
| 404 | gmb_session_not_found |
No connect session with that id, or it is not this account’s (identical for both); on the public connect page, an unknown link |
| 410 | gmb_session_expired |
A connect link’s 30 minutes ran out. Create a new session. Shown on the public connect page; the API reports an expired session as pending past its expiresAt |
| 409 | gmb_session_state |
The connect session is not in a state that step allows: it was already completed, cancelled or failed. Shown on the public connect page |
| 409 | gmb_redirect_exists |
That return address is already registered |
| 502 | gmb_upstream_error |
Google answered with an error or something MauKirim cannot interpret. Retry later |
| 503 | gmb_unconfigured |
The connector is not configured on this deployment (no Google client or credential key) |
| 400 | invalid_input |
Also: a field that cannot be changed, a value outside its limit (title 100, description 750, reply 4096 characters; a post’s 1,500-character summary, https-only links and photo URLs, a CALL button with a URL; a metric, date range or month; an attribute name; a path outside the Google door’s allow-list), a bad status, limit, pageSize, maxRating, externalId or date, and a request Google itself rejected as invalid |
| 404 | not_found |
Also: a Google Business job that is not this account’s, and a return address that is not this account’s |
| 429 | rate_limited |
Also: more than 120 connect sessions created in an hour. Carries Retry-After and retryAfterSeconds |
A gmb_rate_limited response looks like this:
HTTP/1.1 429 Too Many RequestsRetry-After: 7
{ "ok": false, "code": "gmb_rate_limited", "retryAfterSeconds": 7 }See Google Business rate limits for what to do about it.
Anything else
Section titled “Anything else”| HTTP | code |
Condition |
|---|---|---|
| 404 | not_found |
A referenced record does not exist for this account |
| 500 | server_error |
Unexpected — record it and contact support with the time and the request |
Rate limits, and their absence from responses
Section titled “Rate limits, and their absence from responses”No /api/v1 response sets an X-RateLimit-* header. Retry-After appears only on the Google Business
endpoints: a 429 gmb_rate_limited and the 429 rate_limited of creating a connect session. Both come with
retryAfterSeconds in the body.
The other limit reachable from this API is OTP issuing, which sets no header:
| Limit | Value | Keyed by |
|---|---|---|
| OTP issue | 5 per hour | Your account and the destination phone number together |
POST /otp/verify has no rate limit of its own; it is bounded by the challenge’s 5-attempt budget instead
(see Send and verify an OTP). Sign-in, signup and password-reset limits exist but
belong to the dashboard’s session endpoints, not this API.
Because counts are never returned, treat an OTP 429 as “back off and try later” rather than parsing a number
out of a header. A Google Business 429 tells you how long: wait Retry-After seconds. A verification flow that retries on 429 with a short delay is usually fine; one that loops
immediately is not.
Retrying
Section titled “Retrying”| Situation | Safe to retry? |
|---|---|
503 worker_offline, 502/timeouts |
Yes — the request very likely did nothing. Retry with the same Idempotency-Key. |
402 insufficient_credits |
Not until the balance is topped up |
409 batch_idempotency_conflict |
No — you reused a key with a different body. Use a new key for a genuinely new request. |
429 rate_limited |
Later, not immediately |
429 gmb_rate_limited |
Yes, after Retry-After seconds, with the same Idempotency-Key on a write |
429 rate_limited on creating a connect session |
Yes, after Retry-After seconds; reuse a still-pending session instead of creating many |
410 gmb_session_expired, 409 gmb_session_state |
No: create a new connect session |
502 gmb_upstream_error |
Yes, with backoff and the same Idempotency-Key |
409 gmb_reconnect_required |
Not until the account owner reconnects in the dashboard |
500 server_error |
Yes, once, with the same key — then report it |