Errors

Created by Kalin Ivanov, Modified on Thu, 18 Jun at 7:21 PM by Kalin Ivanov

SwissPay returns conventional HTTP status codes plus a JSON body with a stable error.code and a human-readable error.message.

{
  "error": {
    "code": "invalid_params",
    "message": "amount must be a positive integer"
  }
}

The code is stable — match against it in your code. The message may evolve to be clearer over time and should be treated as human-readable only.

Error catalogue

HTTP code When you'll see it
400 missing_idempotency_key No Idempotency-Key header on POST /payments.
401 missing_api_key No Authorization header.
401 invalid_api_key Unknown or revoked key.
404 customer_not_found Payment referenced an unknown cus_..., or one belonging to a different merchant.
404 (no body) Any other unknown ID (pay_..., etc.) returns 404 without a body.
409 key_reused Same Idempotency-Key, different request body.
422 invalid_params Validation failure — see Common 422 causes below.
422 customer_email_taken Email already on file for this merchant (unique per merchant, case-insensitive).
422 customer_external_id_taken external_id already on file for this merchant.
422 provider_not_configured Your account has no active payment-provider connection.
502 / 503 provider_error The upstream payment provider failed or timed out.

Decline handling

Declined card payments return HTTP 200, not a 4xx. We do this because the API call itself succeeded — the provider received the authorisation request and the issuer made a decision. The decision happened to be "no".

{
  "id": "pay_01HABC...",
  "status": "failed",
  "failure": {
    "code": "refused",
    "reason": "Refused by issuer"
  },
  "amount": 2999,
  "currency": "CHF"
}

If your test framework asserts only on HTTP status, declines will silently pass as successes. Always check status first:

res = requests.post(url, json=body, headers=headers)
res.raise_for_status()              # 4xx / 5xx → exception
data = res.json()
if data["status"] != "succeeded":   # 200 OK with failure
    handle_decline(data["failure"]["code"], data["failure"]["reason"])

Failure codes

When status: "failed", the failure.code will be one of:

Code Meaning
refused Generic issuer refusal — most common.
expired_card The card on file is past its expiry.
insufficient_funds Self-explanatory.
lost_card, stolen_card, pickup_card Card reported in the issuer's risk lists.
3ds_failed The cardholder failed the 3-D Secure challenge.
3ds_abandoned The cardholder never completed the challenge.
3ds_token_expired The cardholder arrived at the challenge page after the 15-minute window.
3ds_not_available Your connection is in 3DS-required mode but the issuer can't perform 3-D Secure.

Common 422 causes

  • amount ≤ 0 or non-integer.
  • Bad email (failed RFC 5322 validation).
  • locale not in xx-XX form.
  • metadata exceeds 20 keys or a value exceeds 500 chars.
  • payment_method.holder_name missing (required on every card payment).
  • success_url / failure_url missing or non-HTTPS when 3-D Secure is enabled.

What to log

For every API request, log the request ID we return in the Swisspay-Request-Id response header. If something goes wrong and you contact support, quoting that ID gets you to a resolution faster than any other piece of information.

Retries

  • 5xx and connection timeouts: safe to retry with the same Idempotency-Key.
  • 4xx: don't retry — the request is malformed; fix it first.
  • 200 status: failed (decline): don't retry the same card. The customer can choose a different payment method.

Was this article helpful?

That’s Great!

Thank you for your feedback

Sorry! We couldn't be helpful

Thank you for your feedback

Let us know how can we improve this article!

Select at least one of the reasons
CAPTCHA verification is required.

Feedback sent

We appreciate your effort and will try to fix the article