merchant_not_charge_capable
A typed
422 Unprocessable Entityresponse returned byPOST /v1/payment_intentswhen a tenant on your platform has not finished payment-provider onboarding. Carries a signedsetup_urlyou can hand back to the tenant to send them straight to the Mooov screen that finishes their setup.
This page covers the response shape, the lifetime of the embedded
setup link, the security properties of the link itself, and the
recommended UX on your side. The broader /v1/payment_intents
contract lives in the Connect protocol reference.
When you hit it
You call POST /v1/payment_intents on behalf of a tenant who has
connected to your platform (a valid Mooov-Merchant header pointing
at an active merchant_grants row) but the tenant has not finished
PSP onboarding (no merchant_provider_accounts row with
charges_enabled = true).
The most common path is:
- The tenant clicks Connect on your consent screen.
- The tenant runs through Mooov's signup wizard but skips the PSP step (it is deferrable by design — getting a treasurer in front of Stripe's KYC flow takes coordination).
- Your code mints a grant and tries to take a payment.
- Mooov returns
422 merchant_not_charge_capablewith asetup_urlyou can use to send the tenant back to finish.
This is intentional. We minted the grant when the tenant consented so your integration doesn't get blocked on the slowest part of PSP onboarding. The trade-off is that the first charge after a deferred wizard returns this error.
Response shape
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"error": {
"type": "merchant_not_charge_capable",
"code": "merchant_not_charge_capable",
"message": "This merchant has not completed payment provider onboarding. Send the merchant admin to setup_url to finish.",
"merchant_id": "merch_lodge_001",
"setup_url": "https://mooov3.mooov.money/signup/onboarding/providers?setup=eyJhbGciOi...",
"setup_url_expires_at": "2026-05-20T13:00:00.000Z",
"providers": [
{"id": "prov_stripe", "display_name": "Stripe", "status": "not_connected", "source": "global"},
{"id": "prov_mollie", "display_name": "Mollie", "status": "not_connected", "source": "global"}
],
"docs_url": "https://docs.mooov.money/errors/merchant_not_charge_capable"
}
}
Field reference:
| Field | Always present | Notes |
|---|---|---|
type |
Yes | Stable string. Wire on this rather than the human message. |
code |
Yes | Equal to type for this error. Distinguish from infrastructure errors which set code separately from type. |
message |
Yes | Human-readable. Subject to copy iteration; do not parse. |
merchant_id |
Yes | The tenant the call was made on behalf of (echoes Mooov-Merchant). |
setup_url |
Yes | Signed, single-use link. When Mooov cannot identify the tenant's owner-role user (rare), this degrades to a non-signed variant that requires the tenant to sign in to Mooov first; see "The setup_url" below. |
setup_url_expires_at |
Yes | RFC3339 UTC with a Z suffix. Millisecond precision for signed links; second precision for the degraded variant. |
providers |
Yes | Catalog of payment providers available to the tenant, with their connection status. May be empty if the catalog is unavailable. |
docs_url |
Yes | Stable link to this page. |
The envelope is intentionally additive: future fields will appear
inside error.* without removing existing ones, so a forward-
compatible client only needs to switch on error.type.
Provider entries
Each row in error.providers describes one PSP the tenant could
connect on Mooov's side:
| Field | Notes |
|---|---|
id |
Stable identifier (prov_stripe, prov_mollie, etc.). |
display_name |
Marketing name for the PSP. Safe to render verbatim in your UI. |
status |
One of not_connected, in_progress, connected, unavailable. unavailable means the PSP is not offered to this tenant today. |
source |
Why the PSP is in the list. global (operator catalog) for live catalog rows; the degraded setup_url variant ships a single static entry with source platform_allowlist. A future release will introduce further per-platform allowlist values such as blocked_by_platform_allowlist. |
You can ignore the providers list entirely and still recover from
this error — the setup_url is sufficient. The list exists so you
can render a richer "Mooov needs
The setup_url
setup_url is a single-use signed link to Mooov's onboarding wizard.
Clicking it lets the tenant's owner-role user complete PSP setup
without signing into Mooov first. Internally the URL embeds a
JWT-style token, signed with a secret only Mooov holds, that:
- binds to the specific
merchant_idin the error body; - binds to the merchant's owner-role user (so revoking that user via the operator console immediately invalidates outstanding links);
- has
scope = psp_setup, so even if the link is replayed in an unexpected context it cannot be used to mint a full portal session or move money on behalf of the merchant; - has a 1-hour TTL —
setup_url_expires_atis the wall-clock upper bound; - is single-use — Mooov records the JTI on the first redemption and
refuses every subsequent redemption with
410 token_unusable.
The redemption endpoint is
POST /v1/public/onboarding/exchange-setup-token. Your integration
does NOT need to call this directly: Mooov's merchant SPA detects
the ?setup= query parameter on /signup/onboarding/providers
and exchanges it on the tenant's behalf. The tenant lands on Step 2
of the wizard with the PSP catalog already visible.
The degraded variant
In the rare case where Mooov cannot identify the tenant's owner-role
user, setup_url carries ?merchant_id= instead of ?setup=. The
link points at the same wizard step but is not signed and not
single-use: a tenant user who is already signed in to Mooov lands
directly on the PSP step, anyone else sees the Mooov login screen
first. Treat both variants the same way on your side; the only
contract difference is that the security properties above apply to
the signed variant only.
Do not cache or share
- Treat
setup_urlas a per-charge artifact. Do not store it across requests. The 1-hour TTL is a ceiling, not a target; we may tighten it. - Do not log it. It is, for one hour, a credential.
- Do not forward it through your own redirector. The URL is signed for end-user redemption; round-tripping it through a server-side middleman risks exposing it in your access logs.
- Do not share it across users. The token binds to one specific Mooov merchant_user. Forwarding it to a different employee in the same lodge will exchange successfully but record the wrong user in our audit trail.
Recommended UX on your side
The end-user journey we have in mind:
- Your tenant takes a guest's payment. Your API call to Mooov fails with this error.
- You surface a clear message to the user, ideally on the same screen they triggered the charge from: "Payments are not set up yet. Finish setup → .
- The button opens
setup_urlin a new tab (or top-level navigation — both work). The lodge admin completes Stripe onboarding on Mooov. - When the lodge admin returns to your dashboard, retry the charge. Successful charges no longer return this error.
If the click happens past setup_url_expires_at, the SPA shows
a friendly "This setup link is no longer valid" panel. The tenant
can ask you to generate a new link by attempting another charge
that hits the same error path. Each new failed charge mints a
fresh link.
Notes on related errors
| Error | When | Setup link in body? |
|---|---|---|
merchant_not_charge_capable (this) |
Tenant exists, grant exists, PSP onboarding incomplete. | Yes (when an owner is known). |
MERCHANT_NOT_CONNECTED (legacy) |
Same condition, but the request carried no resolvable merchant ID. Returned with the older simple body for backward compatibility. | No. |
GRANT_NOT_FOUND |
No active merchant_grants row. Tenant probably revoked you in their Mooov dashboard. |
N/A — you need a new grant first. |
CONNECTED_ACCOUNT_LOOKUP_ERROR (500) |
Transient infrastructure error reading PSP rows. Safe to retry; not the merchant's onboarding state. | N/A — this is not the same error. |
merchant_not_charge_capable is the only one of these that
indicates "the tenant can finish their own setup right now". Treat
the others as platform-side / operational issues.