message:
code so your client can branch without string-matching:
Check
service_window on GET /v1/contacts/{contact} before sending to avoid the closed-window refusal.
Stability contract
codevalues are stable once shipped. Build retries and i18n against them.- The
messagestring can change freely. Treat it as display-only. - New codes may be added at any time. Removing a code is a breaking change.
- Validation failures use the Laravel
errorsmap (field → messages), notcode.
errors map:
402 plan limit reached
LazyApp’s plans decide what a workspace can do in live mode. When a request asks for something the plan does not include, the API answers402 Payment Required instead of doing part of the work:
422 with a plain message (and, for numbers, an errors entry on phone_number_id), not plan_limit_reached.
Test keys are never plan-limited. Test mode runs on the shared test number, so a sk_test_ key keeps working on any plan, or none. Build against test, then upgrade before you switch to sk_live_.
The body is the same message + code shape as a send refusal above, with no errors map. Branch on code, not on the status alone: a 402 can also mean insufficient_credits on a partner-managed number. Retrying will not help until the plan changes. See Plans and limits.
Status codes
Which failures are worth retrying
LazyApp already retries retryable provider failures internally, and does not retry policy or invalid-recipient failures — retrying those would burn quota to get the same answer. Apply the same rule to your own client:Retry with backoff
409, 429, 500, 502, 503, and network timeouts. Always with an
Idempotency-Key so a retry cannot double-send.Do not retry
400, 401, 402, 403, 404, 422. The same request will fail the same way. Surface it
and fix the cause. A 402 clears only once the plan or wallet changes.A send can fail after it is accepted
POST /v1/messages returning 201 means LazyApp accepted the message and the provider took it. Delivery is asynchronous: the recipient’s phone may be off, the number may not exist on WhatsApp, or the user may have blocked you.
So treat 201 as accepted, not delivered, and get the outcome from webhooks — message.delivered, message.read, message.failed. See webhook events.
When a send fails immediately and synchronously, you get 422 with the provider’s reason:
Common causes
Free-form message outside the 24-hour window
Free-form message outside the 24-hour window
You can only send free-form content within 24 hours of the contact’s last message.
Outside it, send an approved template. This is enforced
server-side, not just in the UI.
Template not approved
Template not approved
A template must be
approved by Meta before it can be sent. Check status with
GET /v1/templates, and remember approval can be revoked later — a template can move
to paused or disabled on quality grounds.Capability not enabled
Capability not enabled
Calling, block lists and India payments are gated by Meta per account. Check
GET /v1/capabilities instead of assuming.Contact suppressed
Contact suppressed
A contact who opted out is excluded from marketing sends. That is deliberate; do not
work around it.
Best practices
- Branch on
code(and field keys undererrors), never onmessagetext — wording can change. - Handle
429by honouringRetry-After; see rate limits. - Treat provider-style
422bodies as caller-fixable; treat500/502/503as transient. - Prefer webhooks over polling for delivery outcomes.
- Always send an Idempotency-Key when retrying writes.
- Monitor
GET /v1/capabilitiesfor quality and messaging-limit tier changes. - Log the full response body plus
X-Request-Idfor support.
Getting help
Quote theX-Request-Id from the failing response in your email to
support@lazyapp.in. It identifies the exact request, including the provider call it made and the error the provider returned.