Skip to main content
Every error response uses a consistent JSON envelope. Whether you got a 400, 404, 422, or 500, the shape is the same.

Error envelope

Fields

Error types

error.type is one of exactly seven values:

Decision flow: which status code will I get?

Requests are checked in this order. The first gate that fails determines the status code - a request that would fail two ways (say, a malformed body and a missing scope) always fails at the earlier gate.
  1. IP throttle. Too many requests, or too many failed sign-ins, from one IP address: 429 (rate_limited, too_many_failed_attempts).
  2. Request shape. A body that is not valid JSON, is too large or is nested too deeply, or a query string that carries objects: 400 or 413.
  3. API key. A missing, invalid or revoked key: 401.
  4. Account state. An organization that is paused (hibernating), locked, cancelled or not yet active: 403 (organization_*).
  5. Plan. A plan without API access: 403 (plan_no_api_access).
  6. Scope. The key lacks a scope the endpoint needs: 403 (insufficient_scope).
  7. Rate limit. The key is over its budget for the endpoint: 429 (rate_limited).
  8. Idempotency. A missing or malformed Idempotency-Key on a POST: 400. A key reused with a different request, or still in use: 409.
  9. Validation and business rules. The request itself: 400, 404, 409 or 422.
A path that matches no endpoint answers 404 without any of the checks above.
GET /v1/organization is the one exception to the plan and account-state gates above - it always answers, regardless of plan or account state, so a blocked integration can still read account_state and find out why. It still requires a valid key with the organization:read scope.

Common error codes

Branch on error.code. Codes are open-ended: new ones ship over time, so treat any code you don’t recognize as its parent type. Every code below has its own heading, so the doc_url on an error response links straight to its entry.

Authentication (401)

api_key_paused

401. Reserved. The paused key state exists in the schema, but no code path pauses a key and the dashboard has no pause or resume control, so you will not see this code today.

api_key_revoked

401. This API key has been revoked. Mint a new key from the dashboard at https://app.vexioncards.one/settings/developers.

invalid_api_key

401. The API key does not exist, is malformed, or is not associated with an organization.

missing_api_key

401. Authorization header is required. Send ‘Authorization: Bearer vxn_sk_…’.

Permissions (403)

Every organization_* code here is an account-level gate that blocks the entire request before it reaches a resource handler - it is not specific to writes. The one exception is GET /v1/organization, which always answers regardless of plan or account state, specifically so a blocked integration can still read account_state and find out why. See the Decision flow above and the diagram on the Introduction page.

insufficient_scope

403. The API key does not carry a scope this endpoint requires. The message names the missing scope and lists the scopes the key does have.

organization_cancelled

403. This organization is cancelled. The Public API is unavailable.

organization_locked

403. This organization is locked. The Public API is unavailable; contact support.

organization_not_active

403. This organization is not active yet. The Public API is unavailable until activation, or claiming of the account, is complete.

organization_paused

403. This organization is paused (hibernating). The Public API is unavailable, except GET /v1/organization, until the organization is resumed from the dashboard.

plan_limit_reached

403. The plan’s limit for this resource is already in use. Upgrade the plan or remove records to create more.

plan_no_api_access

403. This organization’s plan does not include Public API access. Upgrade the plan to use /v1.

Validation (400 / 422)

field_immutable

400. A card’s type cannot be changed after creation, and a stamp card cannot switch mechanics.is_prepaid after creation.

invalid_cursor

400. A starting_after or ending_before cursor is malformed, or is a cursor for a different resource.

invalid_date

400. A date or timestamp parameter is neither unix seconds nor an ISO-8601 timestamp.

invalid_json

400. The request body is not valid JSON.

invalid_limit

400. The limit parameter is outside the accepted range (1 to 100).

invalid_query_parameter

400. A query parameter carries a value outside its allowed set. The message names the parameter and lists the values it accepts. Examples: sending both starting_after and ending_before, or a serial_number filter on GET /v1/customers that is longer than 256 characters or shaped like a query operator.

missing_required_field

422. A field this operation requires is absent from the request body. The message names the field and, where the field is an enum, the values it accepts.

parameter_invalid

422. A field or filter value is invalid for this operation. The message names the offending value and why it is rejected.

parameter_missing

400. A required field or body is absent.

parameter_unknown

422. The request carries a parameter this operation does not accept, or one that exists but is not settable here. Remove it. A key inside mechanics that the card type does not have (also inside a reward, a level or employee) answers this code with the full name, for example mechanics.rewards[0].colour.

request_body_too_deep

400. The request body is nested more deeply than the API accepts. Body SIZE is bounded separately and reports request_too_large instead.

request_too_large

413. The request body exceeds the maximum allowed size.

validation_failed

422. One or more fields failed validation. Read field_errors[] for the per-field detail: each entry carries its own param, code and message. The per-field codes are listed under Per-field codes.

Per-field codes (inside field_errors[])

These codes appear in the code of a field_errors[] entry, not as the top-level error.code. The top-level code is validation_failed.

card_id_required

422. The customer is enrolled in more than one card, so POST /v1/transactions cannot tell which card to use. Send card_id.

card_mismatch

422. The card_id sent on POST /v1/transactions does not match the card the customer is enrolled in.

invalid_for_card_type

422. A key in initial_state is not valid for this card type, for example initial_state.stamps_collected on a card that is not a stamp card.

invalid_format

422. The value does not match the expected format, for example a malformed email, URL or id.

invalid_length

422. A string is too short or too long, or an array or object has too few or too many entries.

invalid_value

422. The value is present but not acceptable for this field. On a card it also covers a level name used twice, a level whose spend is not above the level before, a reward with both value and percentage, and a reward value that is not a whole number of minor units.

options_required

422. A custom field of a type that offers choices (select or multiselect) was sent without options.

out_of_range

422. A number is below its minimum or above its maximum. A referral or starting bonus above the limit of the card also answers this code, in field_errors[].code of a validation_failed error, with param referral.referrer_bonus, referral.referee_bonus, mechanics.starting_points or mechanics.starting_stamps. On a cashback card the limit is money: about 50 EUR per bonus in your currency (for example CZK 1 250, HUF 20 000, PLN 200), so the allowed number of points is that amount divided by cashback_point_value. Card money limits answer this code too, with param mechanics.earn_rate_per_unit, mechanics.cashback_point_value, mechanics.tiers or mechanics.rewards: cashback above 30 percent (60 percent per level), more than 20 rewards, points_required below 1, a reward percentage outside 1 to 99, a reward value below 1, a cashback_point_value of 0 or less, or an employee discount that starts below 1 percent. initial_state.points above the starting bonus limit of the card answers it as well. Only a changed value is checked on PATCH.

prepaid_starts_empty

422. A prepaid stamp card must start at 0. Send initial_state.stamps_collected: 0 (or omit it). Stamps are added by a transaction.

Parser-level rejections (400 / 431 / 408)

These are different from every other code on this page: they are produced by the HTTP server before the request reaches the API at all. No route is matched, your API key is never read, and the request is not counted against any rate limit. The response still includes a Vexion-Request-Id, but because the request never reached the API there is no corresponding entry in your API logs to correlate it with. Retrying an identical request will fail identically - the fix is in whatever produced the bytes (your HTTP client, or a proxy in front of it), not in the request’s content.

invalid_request

400. The request was not valid HTTP. In practice this is almost always a header problem: a control character in a header value, an invalid character in a header name, or a bare carriage return in the header block. The usual cause is unsanitised user input interpolated into a header. Strip control characters from any value you place into a header before sending.

request_header_fields_too_large

431. The request’s header block exceeded the maximum size the server accepts. Send fewer or shorter headers. The body size limit is separate and reports request_too_large instead.

request_timeout

408. Reserved for a request that was not completed within the time the server allows for it. Current server versions typically close an idle connection without sending a response body rather than returning this code, so you are unlikely to observe it in practice. It is documented so that a client which does receive it can branch on the code rather than on an unexpected status.

Not found (404)

card_not_found

404 / 422. A referenced card does not exist in your organization. Returned as 404 when the card is the resource you addressed, and as 422 when it is a value inside your request body (for example an enabled_card_ids entry on a location, or audience.card_ids on a broadcast).

location_not_found

404 / 422. A referenced location does not exist in your organization. Returned as 404 when the location is the resource you addressed, and as 422 when it is a value inside your request body (for example location_id on a customer, or a team member’s location_ids).

notification_not_found

404. No notification with that id exists in your organization.

resource_missing

404. The universal not-found code, used by EVERY /v1 resource. A resource that belongs to another organization returns this too, never a 403, so the API never leaks whether an id exists outside your organization.

Idempotency (400 / 409)

idempotency_key_conflict

409. The same Idempotency-Key was reused with a different request body.

idempotency_key_in_use

409. An earlier request with this Idempotency-Key is still processing.

idempotency_key_invalid

400. Idempotency-Key header must be between 8 and 255 characters.

idempotency_key_required

400. No Idempotency-Key was sent on a POST. Optional on PATCH and DELETE, and never raised there.

Rate limit (429)

concurrent_broadcasts_exceeded

429. The organization already has the maximum number of broadcasts in flight. Wait for one to finish before sending another.

deliver_cooldown_active

429. This customer was already sent a wallet link on this channel recently. Try again shortly.

rate_limited

429. The rate limit was hit. Respect Retry-After. Also returned by the pre-authentication per-IP flood ceiling (600 requests per minute per IP), which runs before your API key is checked - see Rate Limits.

rate_limited_per_day

429. The per-day bucket for this operation is exhausted.

rate_limited_per_minute

429. The per-minute bucket for this operation is exhausted.

too_many_failed_attempts

429. Pre-authentication per-IP ceiling: 20 failed authentications (401s) in 15 minutes from the same IP. Runs before your API key is checked; a 403 for a missing scope never counts toward it.

Server (500)

delivery_failed

500. The wallet link could not be delivered on the requested channel. This is a failure in our delivery pipeline, not a problem with your request.

image_ingest_failed

503. The image passed validation but could not be ingested into our storage. This is a server-side failure, not a problem with your image; retry.

internal_error

500. Something failed on our side. Retry-safe with the same Idempotency-Key.

service_unavailable

503. The public API failed to start and is temporarily unavailable due to a server-side configuration error. This is not a problem with your request; retry shortly. Distinct from a 404 on purpose: /v1 answering 503 means the facade is down, not that the route does not exist.

Business-rule violations (mostly 409, some 422)

Semantic violations: the request is well formed, but it asks for something the resource’s own rules forbid. They all carry type: invalid_request_error; the message explains the specific rule. Branch on the code.

action_not_valid_for_card_type

422. Coupon cards support only the “redeem” action.

address_not_geocodable

422. The address could not be resolved to coordinates. Check it, or send coordinates alongside the address.

admin_has_all_locations

422. admin members have implicit access to every location; omit location_ids.

already_member

409. This email is already a member or has a pending invitation.

already_reversed

409. This transaction has already been reversed.

amount_not_valid_for_action

422. A redeem carries no bill. amount_minor on a redeem answers this code, also on a coupon redeem. Remove amount_minor. param: amount_minor.

amount_required

422. amount_minor (bill total in minor units, greater than 0) is required for discount transactions.

amount_too_large

422. amount_minor is above the largest single bill of your currency, or above the maximum transaction amount you set in the dashboard. The largest bill is 10,000 EUR, 250,000 CZK, 4,000,000 HUF, 45,000 PLN, 50,000 RON, 9,000 GBP, 10,000 CHF or 11,000 USD. param: amount_minor. See Money and currency.

audience_invalid

400. An audience.customer_ids entry references a customer that is not in your organization.

broadcast_not_cancellable

409. This broadcast can no longer be cancelled - it is not a pending scheduled send.

cannot_reverse_reversal

409. A reversal row cannot itself be reversed.

card_has_active_customers

409. The card still has active enrolled customers. Archive it first (DELETE without gdpr=true), migrate customers as needed, then retry with gdpr=true.

card_name_already_exists

409. A card template with that name already exists in your organization.

card_type_not_writable

422. Cards of this type cannot be written through the API. A transaction can only be posted against stamp, reward, discount and coupon cards. On membership, gift_card and custom cards, POST /v1/transactions returns this code. You also cannot PATCH the mechanics of a membership or gift_card card. POST /v1/cards never returns this code, because type accepts only the five creatable values and anything else fails schema validation as 422 validation_failed instead.

contact_missing_for_channel

422. The customer has no contact detail on file for the requested channel (no email address for an email delivery, no phone number for an SMS one).

coupon_already_redeemed

409. This coupon has already been redeemed.

coupon_not_reissuable

422. Only a coupon card can be reissued, and this card is a different type.

currency_mismatch

422. The currency you sent on POST /v1/transactions is not the currency of the business. A business has one currency. field_errors[0].expected_currency names the currency to send. You can also leave currency out. param: currency. See Money and currency.

custom_field_in_use

409. The key of a custom field cannot be renamed while a card references the field. Detach it from all cards first.

custom_field_limit_reached

409. The organization is already at its maximum number of defined custom fields.

customer_erased

409. This customer has been erased (GDPR right-to-be-forgotten) and can no longer be modified.

expires_at_in_past

422. expires_at must be a unix timestamp in the future.

expires_at_scanner_only

422. expires_at is available on the scanner role only.

external_id_collision

422. An audience.external_ids value maps to more than one customer. Resolve the duplicate before sending.

external_id_conflict

409. On POST /v1/customers and PATCH /v1/customers/{id}: a customer with this external_id already exists for this card. On POST /v1/transactions: you reused an external_id for a different transaction. A retry of the same transaction is fine and returns the original. A different action, value, amount_minor or currency answers this code. amount_minor and currency are compared only when you send them. param: external_id.

form_builder_missing_email_field

422. form_builder.fields must include a required system email field. The param is form_builder.fields.

form_builder_missing_first_name_field

422. form_builder.fields must include a required system first_name field. The param is form_builder.fields.

geocoding_unavailable

422. Geocoding is unavailable right now. Send coordinates alongside the address.

image_aspect_ratio_invalid

422. The supplied image does not match the aspect ratio this field requires (a 5% tolerance is allowed).

image_format_invalid

422. The supplied image is not a valid PNG. Either the magic-byte signature did not match, or the PNG header or chunk structure is malformed or truncated. Only PNG is accepted; the check reads the bytes, never the Content-Type header. param names the offending field, for example design.icon_url.

image_not_square

422. The supplied image must be square, within a 2% tolerance.

image_resolution_too_large

422. The supplied image exceeds the maximum pixel dimensions for this field.

image_resolution_too_small

422. The supplied image is below the minimum pixel dimensions for this field.

image_too_large

422. The supplied image exceeds the 10 MB maximum file size. The limit is measured on the real streamed bytes, not on a declared length. param names the offending field, for example design.logo_url.

insufficient_balance

409. The customer does not hold enough of the unit being redeemed to cover the requested amount.

invalid_image_url

422. A URL could not be used. An image URL must point to an image that can be fetched over https. terms_of_use.full_url must use https and must not contain credentials. Links with other schemes (javascript:, data:, ftp:, file:, plain http:) are rejected.

invalid_status_transition

409. The requested status is not a legal transition from the card’s current status.

key_already_exists

409. An active custom field already uses this label or this explicit key. The param says which one.

languages_supported_empty

422. languages_supported must contain at least one language.

languages_supported_missing_translations

422. You cannot widen languages_supported to include a language that the card’s form fields have no translations for. Add the translations first.

languages_supported_requires_form_builder

422. This card has no form_builder yet. Include form_builder in the same request as languages_supported, so field translations can be built for it.

levels_invalid_level

422. A level on a reward card needs a multiplier of at least 1 and a spend of 0 or more. param: mechanics.tiers.

levels_need_a_level

422. A reward card has levels switched on (earn_mode: tiered) but no level. Add a level or switch levels off. param: mechanics.tiers.

linked_card_not_found

422. mechanics.linked_card_template does not reference a valid card.

linked_coupon_redeem_unsupported

422. This coupon upgrades into another card on redemption. Redeem it at the point of sale.

location_ids_too_many

422. audience.location_ids accepts at most 25 ids.

mechanics_type_mismatch

422. The mechanics block is required and must match the card’s own type.

member_not_editable

409. The team member is in a terminal state and cannot be edited. Add the person again with POST /v1/team_members.

multiple_locales_unavailable

422. A notification title or body was sent in more than one language. Notifications are delivered in one language to every recipient, so send each in a single language. See Send a notification.

not_invited

409. The team member has no pending invitation, so there is nothing to resend.

occurred_at_out_of_range

422. occurred_at must be a Unix timestamp within the last 90 days and not in the future.

organization_not_linked

409. This organization’s team setup is not connected, so team members cannot be created. Contact support.

owner_immutable

409. The organization owner cannot be modified through the API. Use the dashboard.

product_not_found

422. A product_id on a line item does not match a product in this location catalog. 422. A reusable coupon cannot link an upgrade-on-redeem card.

reversal_window_expired

409. Transactions can only be reversed within 90 days of creation.

scanner_requires_single_location

422. The scanner role requires location_ids with exactly one entry.

schedule_at_out_of_range

422. schedule_at must be in the future and at most 90 days ahead.

sku_already_exists

409. A product with this SKU already exists at this location.

source_not_reversible

409. Only transactions created through the Public API (source “api”) can be reversed. Corrections for other sources happen in their originating channel.

stamp_count_images_language_mismatch

422. A mechanics.stamp_count_images entry declares a different language set than its siblings.

stamp_count_images_length

422. mechanics.stamp_count_images must contain exactly one image per stamp in the card’s goal.

template_invalid

422. Template variables are not supported in the title (it is shared across recipients) - use them in the body.

template_variable_not_supported

422. Reserved. Notifications are plain text today, so any {{ in a body is refused earlier with template_variables_unavailable. You will not see this code today.

template_variable_not_supported_by_card

422. Reserved. Notifications are plain text today, so any {{ in a body is refused earlier with template_variables_unavailable. You will not see this code today.

template_variable_requires_card_scope

422. Reserved. Notifications are plain text today, so any {{ in a body is refused earlier with template_variables_unavailable. You will not see this code today.

template_variables_unavailable

422. A notification body contains template variables such as {{first_name}}. Notification titles and bodies are plain text, and template variables are not available. See Send a notification.

tier_achieve_field_missing

422. A tier in mechanics.tiers is missing the achievement field that tier type requires.

tier_achieve_field_not_allowed

422. A tier in mechanics.tiers carries an achievement field that is not valid for this card.

transaction_not_reversible

422. This transaction record cannot be reversed.

unknown_custom_field

422. The request references a custom field that is not defined on the card. Define it on the card’s form_builder first (see GET /v1/cards/id).

unknown_form_field_reference

422. pass_fields references a form field that is not declared on the card.

unsupported_card_type

422. Enrolling a customer on this card type is not supported through the API. Supported types are stamp, reward, discount, custom and coupon; membership and gift_card are not.

unsupported_target_type

422. An upgrade-on-redeem card must target a Stamp, Reward, Discount or Membership card.

value_required

422. value (whole stamps, greater than 0) is required for stamp transactions; stamps are always an explicit count, spend-driven stamp derivation is not a supported mechanic. On a reward card, value (whole points, greater than 0) is required for a redeem, and an award needs value or amount_minor.

value_too_large

422. An explicit value on POST /v1/transactions is above the limit for the card: 100,000 points on a points card, points worth more than the largest bill on a cashback card, or more than one full card of stamps on a stamp award. param: value. See Money and currency.

webhook_endpoint_limit_reached

409. The organization is already at its maximum number of webhook endpoints. Delete an unused endpoint before registering another.

webhook_event_unknown

422. The request subscribes to an event name that is not in the event catalog.

webhook_url_invalid

422. The webhook url is rejected on its own merits: it is not a valid absolute URL, does not use https, has no host, or resolves to a private or non-publicly-routable address.

webhook_url_unreachable

422. The webhook url is well formed but our probe could not reach it: the host did not resolve, resolved to no address, or refused the connection.

would_drive_balance_negative

409. Reversing this earn would drive the customer’s balance below zero.

MCP server errors

Most problems with the MCP server are one of: the sign-in expired, the access level is too low, or the business is not eligible. Find the message your assistant shows.

Sign-in and access

Your sign-in is missing or expired. Reconnect the VEXiON connector in your AI app and sign in again. In Claude Code, run /mcp and choose vexion.
The key is mistyped, revoked, or sent in the wrong form. The header must read Authorization: Bearer vxn_sk_... with no extra characters. Check that the environment variable behind it is set, and create a new key in Settings > Developer if needed.
The account you signed in with is not an admin of a business that qualifies. Ask an admin of the business to connect, or upgrade the business to VEXiON PRO. If you are an admin of several businesses, sign in with the account that administers the business on VEXiON PRO.
The business is not on VEXiON PRO. The MCP server is part of VEXiON PRO.
Someone revoked the app under Settings > Developer > AI assistants. Remove the connector in your AI app and add it again, then choose the access level once more. Connecting again lifts the revocation.
The assistant tells you the business is unavailable, for example “This business is paused. VEXiON is unavailable until it is resumed in the dashboard.” Resume or activate the business in the dashboard. For a locked business, contact VEXiON support.
Access follows your role and plan live. It ends within about a minute if you stop being an admin, if the business leaves VEXiON PRO, or if the business is paused, locked or cancelled. Check your role, the plan and Settings > Developer > AI assistants.

Tools and results

The tool needs access you did not grant, so the assistant cannot see it. With OAuth, reconnect and choose Read and write, or allow the area under Customize (see Permissions). With an API key, create a key with the scopes the tool needs (see Tools).In Claude, also check that VEXiON is switched on for the conversation: click + at the lower left of the chat, then Connectors.
You choose the businesses on the VEXiON consent page when you connect the app. When the app can use several businesses, the assistant asks which one you mean unless you named it. Tell it the business name. It then passes that business’s organization_id on every call (see Several businesses). An older connection with no stored choice gets the select_organization fallback instead. To add or remove a business, reconnect the app and choose again on the consent page.
The report hit the 30-second or 3,000-row limit. The assistant should continue from the returned next_cursor and add the counts together, or you can ask for a shorter date range, for example month by month. See Safety and limits.
A notification has one language. Ask the assistant to send one notification per language, each to the customers who speak it. See Sending in more than one language.
A confirmation works once, for 10 minutes, and only for the exact details previewed. Ask the assistant to preview again, check the preview and confirm.
If a confirmed action fails in a way that leaves the result unknown, the assistant is told it may already have been done and must not repeat it. Ask it to check first, for example by listing recent notifications or opening the transaction, before trying again.

Limits

You hit a rate limit. Wait for the time the response gives, then continue. Large reports use many reads at once, so leave a minute between them. The numbers are on Rate limits.

Connection problems

Check that the URL is exactly https://api.vexioncards.one/mcp and that your client supports Streamable HTTP. Clients that only support the older SSE transport cannot connect. If your computer or network is managed by your organization, ask your IT team to allow api.vexioncards.one.
Restart the AI app or reload the MCP servers, then reconnect. In Claude Code, run claude mcp list to see the state of the server.
Still stuck? Email VEXiON support from the address on your account and include the time of the problem and the message you saw. Do not send API keys.

Debugging failed requests

1

Copy the request_id

It’s in both the JSON body and the Vexion-Request-Id response header.
2

Check field_errors[]

It pinpoints the exact fields that failed.
3

Contact support

Email support@vexioncards.one with the request_id - we can trace exactly what happened.

Example handling