Geldstuck uses standard HTTP status codes and returns a consistent JSON error body. You never need to parse free-text messages - every error has a stable code.

Error shape

{
  "error": {
    "type": "validation_error",
    "code": "email_invalid",
    "message": "The provided email address is not valid.",
    "param": "email",
    "requestId": "req_01HX3ZE..."
  }
}
FieldDescription
typeBroad category. One of authentication_error, permission_error, validation_error, not_found, conflict, rate_limit_error, api_error.
codeStable, machine-readable identifier. Switch on this in your code.
messageHuman-readable summary. Safe to surface in logs; do not show to end users without localization.
paramField name that caused the error (when applicable).
requestIdEchoed from the x-request-id response header. Include this when filing a support ticket.

HTTP status codes

CodeMeaningRetry?
200 / 201Success.-
400Bad request. validation_error in the body.No - fix the request.
401Authentication failed. Missing, invalid, or revoked key.No.
403Authenticated, but the key lacks permission.No.
404Resource not found, or not visible to this tenant.No.
409Conflict. Duplicate resource, or illegal state transition.No.
429Rate limited.Yes, with exponential backoff - honor Retry-After.
500Geldstuck had a problem.Yes, with backoff.
503Temporary outage.Yes, with backoff.

Common error codes

Authentication

CodeWhen
key_missingNo x-api-key / x-api-secret header.
key_invalidKey pair does not match any active key.
key_revokedKey has been revoked.
signature_invalid(Webhook handler) Geldstuck-Signature did not match.

Validation

CodeWhen
email_invalidEmail failed RFC 5322 validation.
field_requiredRequired field missing - param names the field.
field_too_longparam exceeded its max length.
enum_invalidparam is not one of the allowed values.

Resource

CodeWhen
resource_not_foundThe object doesn’t exist, or belongs to a different tenant.
duplicate_emailTrying to create a user with an email that already exists for this tenant.
kyc_already_completedKYC record has been finalized and can’t be resubmitted.
transaction_not_pendingTransaction is no longer in a state that allows the requested action.

Rate limits

CodeWhen
rate_limit_exceededYou’ve exceeded the per-key rate limit. Retry after Retry-After seconds.

Handling errors idiomatically

const res = await fetch(`${BASE}/tenants/add-user`, {
  method: "POST",
  headers: authHeaders,
  body: JSON.stringify({ name: "Ada", email: "ada@example.com" }),
});

if (!res.ok) {
  const { error } = await res.json();

  if (error.code === "duplicate_email") {
    // Merge with the existing user
  } else if (error.type === "rate_limit_error") {
    const wait = Number(res.headers.get("retry-after") ?? 1);
    await new Promise((r) => setTimeout(r, wait * 1000));
    // retry
  } else {
    logger.error({ requestId: error.requestId }, error.message);
    throw new Error(error.message);
  }
}
Always log requestId. When you email support, we can pull the exact request, response, and timing from a single ID.