Skip to main content

Errors

Every failure generated by Joinotify uses the same envelope:

{
"error": {
"message": "A human-readable sentence about what happened.",
"type": "stable_code"
}
}

Program against type — it is stable. message is written for humans and its wording can change without notice.

Optional fields​

A few fields show up when they apply.

issues — validation failures​

422 Unprocessable Entity
{
"error": {
"message": "Invalid request payload.",
"type": "invalid_request",
"issues": [
{
"code": "too_small",
"path": ["body"],
"message": "String must contain at least 1 character(s)"
}
]
}
}

meta — an error relayed from Meta​

When the refusal comes from Meta, we return their original body untouched. It carries the code, error_subcode and fbtrace_id that Meta support asks for.

422 Unprocessable Entity
{
"error": {
"message": "The Meta Cloud API rejected the request.",
"type": "meta_error",
"meta": {
"error": {
"message": "(#131047) Re-engagement message",
"type": "OAuthException",
"code": 131047,
"error_subcode": 2494010,
"fbtrace_id": "AbCdEfGhIjK"
}
}
}
}

reason and action — no entitlement​

A 402 says why access was blocked and what the way out is: what to do (action.kind) and the dashboard page where it is done (action.url). Show both to whoever runs the integration — in an error log, "payment required" without a link is a dead end.

402 Payment Required
{
"error": {
"message": "Your free trial has ended. Subscribe to keep using the API.",
"type": "payment_required",
"reason": "trial_ended",
"billable_numbers": 1,
"action": {
"kind": "subscribe",
"url": "https://app.joinotify.com/checkout"
}
}
}
action.kindWhen
subscribeTrial ended, subscription cancelled or none at all
update_payment_methodThe last payment failed (past_due)
pay_invoicesThere are unpaid invoices (unpaid)
finish_checkoutThe first payment has not been confirmed yet (incomplete)
resume_subscriptionThe subscription is paused (paused)
renew_licenseThe license has expired (license_expired)
upgrade_planThe plan's monthly message cap was reached (message_limit)
manage_numbersMore numbers are connected than the account pays for (capacity_exceeded)

The block does not arrive unannounced. During the grace period requests keep going through, and the response carries the X-Joinotify-Billing-Warning header with the reason — past_due, for instance. Log that header: it is what tells you a 402 is on its way.

allowed — method not accepted​

A path that exists, called with the wrong method, answers 405 rather than 404. The accepted methods come in the Allow header and in error.allowed, so you look for the mistake in the verb and not in the URL.

405 Method Not Allowed
{
"error": {
"message": "PUT is not supported on this route. Allowed: DELETE, GET, HEAD.",
"type": "method_not_allowed",
"allowed": ["DELETE", "GET", "HEAD"]
}
}

request_id — unexpected failure​

A 500 doesn't say what broke — the cause stays in our logs, never in the response. What it carries is request_id, the same value as the x-request-id header. Send that id to support and we'll find the failure.

500 Internal Server Error
{
"error": {
"message": "Internal server error.",
"type": "internal",
"request_id": "0d72872e-f70e-4c25-95c6-6bec4d33a800"
}
}

Code table​

HTTPtypeMeaningWhat to do
400invalid_requestNo resource ID after /v1/.Fix the path.
400invalid_requestThe body isn't valid JSON, or the path has a malformed % sequence.Fix the body or the URL encoding.
401authenticationToken missing, malformed, unknown or revoked.Check the header and the key.
402payment_requiredNo entitlement: trial ended, payment pending, plan cap reached…Follow error.action.
403tenant_inactiveAccount suspended or cancelled.Contact support.
403forbiddenThe resource does not belong to your account.Confirm the phone_number_id / waba_id.
403forbidden_endpointPath on the proxy's block list.See Meta compatibility.
404not_foundRoute, key, endpoint or delivery does not exist.Check the ID.
405method_not_allowedThe path exists, but not with that method.Use one of the methods in Allow.
409no_phone_numberNo number connected and from omitted.Connect a number or pass from.
409no_wabaNo business account connected and wabaId omitted.Connect a WABA or pass wabaId.
409idempotency_in_progressA request with the same Idempotency-Key is still running.Wait and repeat with the same key.
422invalid_requestPayload failed validation.Read issues.
413payload_too_largeThe body is larger than the route accepts.Split the batch or shrink the body.
415unsupported_media_typeThe body's charset or Content-Encoding isn't accepted.Send JSON in UTF-8, uncompressed or with gzip.
422meta_errorMeta refused (4xx).Read error.meta.
422idempotency_key_reusedThe Idempotency-Key was already used with another body.Use a new key per operation.
429rate_limitCategory limit exceeded.Wait for Retry-After. See Rate limits.
500internalUnexpected failure on our side.Retry; if it persists, send us the request_id.
502meta_errorMeta answered with a 5xx.Retry with backoff.
502upstream_errorWe could not reach Meta.Retry with backoff.
504upstream_errorMeta took longer than 30 seconds.Retry with backoff.

Our error or Meta's?​

One simple rule: if the error.meta field is there, the refusal came from Meta. Joinotify only relayed it.

That matters for what you do next. invalid_request means your payload is wrong and an identical retry will fail again. meta_error is usually a WhatsApp business rule — the 24-hour window closed, an unapproved template, an invalid number — and the fix is different.

On the /v1/* mirror the rule changes: when Meta responds, you get their response, in their format, without our envelope. The Joinotify envelope only appears for failures that happen before the call.

Tracing a request​

Every response carries x-request-id. If you send your own x-request-id, we echo it — which makes it easy to line up logs on both sides.

Your own id is only accepted if it has 8 to 128 characters, from letters, digits, ., _, : and -. Otherwise we generate one of ours (a UUID) — always trust the one that comes back in the header.

curl -i https://api.joinotify.com/templates \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'x-request-id: order-1042-attempt-1'

Keep that identifier in your logs. It's the first thing support asks for.

Which errors are worth retrying​

SituationRetry?
429 rate_limitYes, after Retry-After.
500 · 502 · 504Yes, with exponential backoff and a cap on attempts.
409 idempotency_in_progressYes, after a few seconds, with the same key.
402 payment_requiredNo. Sort out what error.action points to.
401 · 403 · 404 · 405 · other 409sNo. A retry fails the same way.
400 · 413 · 415 · 422 invalid_requestNo. Fix the payload.
422 meta_errorDepends on Meta's code. A window or template error won't fix itself.