Moosyl logo

Errors

What the Moosyl API returns when a request fails, what each status means, and how to handle and retry errors safely.

When a request fails, the API responds with a non-2xx status and a short plain-text body that describes the problem. Validation errors are the exception: their body is JSON (sent as text/plain, so parse it yourself).

Response bodies

Most errors return the message as text:

HTTP/2 401
content-type: text/plain

Invalid API key

Validation errors (a missing or wrong-typed field) return 422 with a JSON body. Show summary to developers and use property to find the field:

{
  "type": "validation",
  "on": "body",
  "property": "/amount",
  "message": "Expected required property",
  "summary": "Property 'amount' is missing",
  "expected": { "transactionId": "", "amount": 0 },
  "found": { "transactionId": "order_123" }
}

on is where the problem is: body, query or params. The body can also include an errors array with every failing field.

HTTP statuses

StatusMeaningRetry?
200Success.
400The request can't be processed as sent, for example a missing pass code or an amount of 0.No. Fix the request.
401The API key is missing or invalid.No. Fix the key.
403Your key is valid but not allowed here, usually a publishable key on a route that needs the secret key.No.
404The resource doesn't exist in this key's environment (Sandbox and Production are separate).No.
409The request conflicts with the current state: a reused transactionId, an already-paid request, or a closed checkout session.No. Read the existing resource instead.
422The request body, query or path parameters failed validation.No. Fix the field.
500Something failed on Moosyl's side. The body is always Internal server error.Yes, with backoff.
502The bank app's service failed. The message is safe to show to your customer.Yes, after a short wait.

Common errors

StatusMessageCauseFix
401API key is requiredNo Authorization header.Send your key as Authorization: <key>, without Bearer.
401Invalid API keyThe key doesn't exist or was deleted.Copy the key again from API Keys in the dashboard.
403Unauthorized accessA publishable key was used on a secret-only route (creating payment requests, reading a payment, customers, products, prices, subscriptions, invoices).Call this route from your server with the secret key.
403Only secret API keys can create checkout sessionsA publishable key was used to create a checkout session.Create the session on your server with the secret key.
404Payment Request not found / PaymentRequest not foundNo payment request with that ID or transactionId in this environment.Check the ID and that you're using a key from the same environment.
404Payment not found, Checkout session not found, Customer not found, Product not found, Price not found, Subscription not found, Invoice not foundThe resource doesn't exist in this environment or organization.Check the ID and environment.
422Validation body (see above)A required field is missing or has the wrong type.Fix the field named in property.
400Pass code is required for BankilyA Bankily payment was submitted without the customer's payment code.Collect the code in your payment UI. The SDK payment screens do this for you.
400Invalid phone numberThe phone number isn't a valid Mauritanian number.Send the 8-digit local number, for example 22222222.
400Amount must be a positive number / Price amount must be a positive numberamount is 0 or negative.Send a whole amount in MRU greater than 0.
400Amount is required when transactionId does not match an existing payment requestA checkout session was created with a new transactionId and no amount.Pass amount, or use an existing paymentRequestId.
400Simulated transfers are only available in sandboxsimulate-transfer was called with a Production key.Use a Sandbox key. See Testing.
409A payment request with transactionId "…" already exists in this environmentPOST /payment-request with a transactionId you already used.Fetch it with GET /payment-request/by-transaction/{transactionId} instead of creating it again.
409Payment request is already paidYou tried to pay, or open checkout for, a request that's already paid.Treat the order as paid. Check its payments instead of starting a new one.
409Checkout session is not openThe session was completed, cancelled or has expired.Create a new checkout session.
502Payment failed due to a third party error, please try again laterThe bank app's service rejected or didn't answer the request.Ask the customer to try again in a moment.

Handle errors with moosyl-sdk

moosyl-sdk throws an AppException for failed requests. It isn't exported, so check error.name. Its fields:

  • message: the API's message (for validation errors, the first issue's message).
  • status: the HTTP status, when the API answered.
  • code: connection_error, timeout (requests time out after 60 seconds) or abort_error when the request never got an answer; otherwise the plain-text body or unknown.
  • Helpers: isClientError, isServerError, isConnectionError, isTimeout.
import { Moosyl } from "moosyl-sdk";

const moosyl = new Moosyl(process.env.MOOSYL_SECRET_KEY!);

try {
  const session = await moosyl.createCheckoutSession({
    transactionId: "order_123",
    amount: 2500,
    successUrl: "https://example.com/orders/123/paid",
    cancelUrl: "https://example.com/orders/123",
    expiresInMinutes: 30,
  });
  // redirect to session.checkoutUrl
} catch (error) {
  if (error instanceof Error && error.name === "AppException") {
    const { status, message, code } = error as Error & { status?: number; code: string };

    if (code === "connection_error" || code === "timeout") {
      // No answer from Moosyl: safe to retry this call (see below).
    } else if (status === 401 || status === 403 || status === 404) {
      // Wrong key, key type or environment: fix configuration, don't retry.
    } else {
      console.error("Moosyl error", status, message);
    }
  }
  throw error;
}

Webhook signature failures throw WebhookSignatureError instead. See Webhooks.

Retrying safely

Your transactionId is what makes retries safe: it's unique per environment, so Moosyl never creates two payment requests for the same order.

  • Checkout sessions are safe to retry. Creating a session with a transactionId that already has a payment request reuses that request, and an open session for it is reused too.
  • POST /payment-request is not idempotent. A second call with the same transactionId returns 409. If a create call timed out, first look the request up with GET /payment-request/by-transaction/{transactionId}, and only create it if that returns 404.
  • Retry connection errors, timeouts, 500 and 502, with a short backoff (for example 1 s, 2 s, 4 s).
  • Don't retry 400, 401, 403, 404, 409 or 422 without changing the request.
  • Payments: don't resubmit a payment just because the call failed. Check the payment request's payments, or wait for the webhook, before asking the customer to pay again.

Use one transactionId per order

Generate transactionId from your own order ID, store it before calling Moosyl, and reuse it on every retry. That way a retry finds the existing payment request instead of creating a duplicate.

Webhook delivery failures

If your endpoint is down or returns an error, Moosyl retries the delivery. What's retried, how often, and how to skip duplicates is on the Webhooks page.

Errors | Moosyl Docs