Skip to main content

Errors

Every non-2xx response uses the RFC 9457 Problem Details envelope, served as Content-Type: application/problem+json.

The envelope

{
"type": "https://api.valueverde.de/errors/cooperative-not-found",
"title": "Cooperative not found",
"status": 404,
"detail": "cooperative 11111111-1111-4111-8111-111111111111 not found",
"instance": "/cooperatives/11111111-1111-4111-8111-111111111111",
"code": "COOPERATIVE_NOT_FOUND",
"trace_id": "6d3a9b1e-8c2f-4a7d-9e5b-1f3a2c4d5e6f"
}
FieldTypeNotes
typeURIStable identifier for the problem class. The path after /errors/ is the lowercase-kebab form of code.
titlestringShort human-readable summary. May change wording across versions — do not branch on this.
statusintegerMirrors the HTTP status.
detailstringOccurrence-specific detail (offending id, the field that failed, etc.).
instanceURIThe request path that produced the error.
codestringStable machine-readable error code. Branch on this.
trace_idstringMirrors the X-Request-Id response header. Quote it in support tickets.
errorsobjectPer-field validation detail — only present on 422 VALIDATION_ERROR.
required_scopesarrayScopes the endpoint required and your token lacked — only present on 403 INSUFFICIENT_SCOPE.

Error codes by status

The codes below are the ones reachable on the public partner endpoints under /v1. The platform returns additional codes on internal endpoints — those are not part of the partner contract.

400 Bad request

codeWhenResolution
VALIDATION_ERRORUnknown sort, dividend_type, or affiliation token; non-numeric min_share_price / max_share_price.Use the documented enum values; check the pagination page for sort tokens.

The token endpoint is the one exception to this list: POST /oauth2/token answers with the standard OAuth2 error object (error, error_description), not Problem Details.

errorWhenResolution
invalid_scopeYou requested a scope your client was not granted. The whole request fails — you get no token, not a narrower one.error_description names both the refused scope and the ones your client holds. Request a subset, or omit scope to receive all of them.
invalid_clientclient_id / client_secret rejected (HTTP 401).Verify the credential pair.
unsupported_grant_typeAnything other than client_credentials.This API supports only client_credentials.

401 Unauthorized

codeWhenResolution
(no code)Missing or invalid Authorization: Bearer <token> on a resource endpoint. Returned by the OAuth2 resource server filter; envelope follows the OAuth2 WWW-Authenticate convention.Mint a fresh access token.

403 Forbidden

Two different failures share this status, and the code is what separates them. Branch on it: one is fixable by minting a different token, the other is not.

codeWhenResolution
INSUFFICIENT_SCOPEThe token is valid but lacks a scope the endpoint requires. required_scopes names exactly what is missing, and the response carries WWW-Authenticate: Bearer error="insufficient_scope", scope="…".Re-mint with that scope. If your client was never granted it, ask partner support to widen the client — your credentials are not rotated by a scope change.
FORBIDDENNot a scope problem: the caller is not permitted to perform this operation at all (e.g. a non-partner token on a partner endpoint). No required_scopes, no WWW-Authenticate.Adding scopes will not help. Check you are calling with a partner client_credentials token.
{
"type": "https://api.valueverde.de/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This token is missing the investors:write scope. Request it when minting the token; if the client was never granted it, contact partner support.",
"instance": "/v1/investors",
"code": "INSUFFICIENT_SCOPE",
"required_scopes": ["investors:write"],
"trace_id": "..."
}

Use GET /v1/me to see what your client is allowed to request before guessing.

404 Not found

The investing endpoints collapse "doesn't exist" and "isn't yours" into a single 404 — acting on an investor or order another partner owns is indistinguishable from one that never existed (no cross-tenant existence oracle).

codeWhenResolution
COOPERATIVE_NOT_FOUNDThe id is unknown, or the cooperative is unpublished.Verify the id; unpublished cooperatives never surface to partners.
MANAGED_INVESTOR_NOT_FOUNDThe investor id is unknown, or belongs to a different partner.Verify investor_id; provision the investor first if you haven't.
SHARE_PURCHASE_NOT_FOUNDThe purchase id is unknown, belongs to a different investor, or sits under an investor you don't own.Verify the investorId / purchaseId pair.

409 Conflict

Preconditions and state-machine violations on the investing write path. None are retryable as-is — fix the precondition first.

codeWhenResolution
INVESTOR_PROFILE_INCOMPLETEPlacing a purchase before the investor's profile is complete.Read missing_profile_fields from GET /v1/investors/{investorId} — it names exactly what is outstanding — then PUT …/profile and retry.
PURCHASE_DEFAULTS_UNAVAILABLEA value the platform derives could not be derived — no sepa_mandate.debtor_iban sent and no banking details on the profile, or the profile records no terms/privacy consent to carry over.detail names exactly which default was missing. Fix the profile, or send the field explicitly.
SHARE_PURCHASE_ILLEGAL_STATECancelling an order that is already settled or decided.The order is terminal; no action will change it.
IDEMPOTENCY_KEY_CONFLICTAn Idempotency-Key was reused with a different request body — including for a different investor, or after the investor's company affiliation changed (which changes the derived applicant_type).Use a fresh key for a new purchase, or resend the original body verbatim to replay it.
MANAGED_INVESTOR_PROFILE_CONFLICTPOST /v1/investors re-used an existing partner_customer_ref with a different inline profile.Use PUT /v1/investors/{investorId}/profile to change a profile; a create endpoint will not overwrite one.

422 Unprocessable entity

Returned for request-body validation failures on the write endpoints (POST /v1/investors, PUT …/profile, POST …/share-purchases). Bean-level checks (max length, email shape, required consent timestamps) and domain value-object checks (IBAN checksum, BIC format, birthdate plausibility) both surface here.

codeWhenResolution
VALIDATION_ERRORA field failed a shape or domain check, or a query parameter was not a recognised value — an unrecognised expand token is rejected here rather than silently ignored.Read errors when present; a parameter-level rejection carries no errors map, only the generic detail.
REPRESENTATION_CONSENT_REQUIRES_COMPANYconsents.representation_authorization.given: true on a profile that carries no company block.Send { "given": false }, or omit it, unless the company section is present. See below.

representation_authorization is the odd one out

The profile write takes four consents under consents. Three of them — terms, privacy, data_sharing — are required and must each be { "given": true }.

representation_authorization is different. It records that someone is authorised to act for a company, so given: true is only valid when the profile carries a company block. For a private individual — the majority case — send { "given": false }, or omit the member entirely.

{
"code": "REPRESENTATION_CONSENT_REQUIRES_COMPANY",
"title": "Representation authorization can only be granted alongside a company affiliation.",
"status": 422,
"errors": {
"profile.consents.representation_authorization.given": "must not be true unless a company section is present"
},
"trace_id": "..."
}

Error keys are paths

Every key in errors is the path you sent, not a bare field name — profile.address.city, profile.bank_account.iban, profile.consents.terms.given. That holds whichever layer rejected the value: a shape check, a section's all-or-nothing rule, and a domain check such as an IBAN checksum all report against the same root, so you can key your error handling off one convention.

On POST /v1/investors and PUT /v1/investors/{investorId}/profile the paths are relative to the request body, which wraps the profile in profile. Elsewhere they are relative to the body's own root — for example statute_consent_given_at on a share purchase.

The body includes per-field detail:

{
"type": "https://api.valueverde.de/errors/validation-error",
"title": "Validation error",
"status": 422,
"detail": "Request body failed validation.",
"instance": "/...",
"code": "VALIDATION_ERROR",
"errors": {
"profile.bank_account.iban": "must be a valid IBAN",
"share_count": "must be at least 1"
},
"trace_id": "..."
}

429 Too many requests

The only 4xx on this page that is retryable as sent — nothing about the request is wrong, there was just too much of it.

codeWhenResolution
RATE_LIMITEDYou exceeded the throttle on /v1 (600 requests a minute per credential) or on POST /oauth2/token (20 a minute).Sleep for Retry-After seconds and send the same request again. If you are hitting it on the token endpoint, cache your access token instead of minting one per call. See Rate limits.

Every /v1 response — not just this one — carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so you can slow down before you get here. The refusal adds Retry-After.

5xx — server errors

codeWhenResolution
(none — generic 500)Unhandled server error.Retry with exponential backoff (1s, 2s, 4s, max 3 attempts). Include trace_id if you need to escalate.

Retry strategy

StatusRetry?
4xx❌ Fix the request. (A 409 IDEMPOTENCY_KEY_CONFLICT specifically means you reused a key with a different body — don't retry that body under that key.)
429✅ The exception among 4xx. Sleep for Retry-After, then resend unchanged. Do not treat it as a 5xx and back off exponentially past the reset — you would be idle while your budget refills.
5xx✅ Exponential backoff with jitter, capped at 3 attempts.
Network timeout✅ Same as 5xx. Reads are idempotent. For POST …/share-purchases, always send an Idempotency-Key so a timed-out retry replays the original order instead of placing a second one; provisioning is idempotent on partner_customer_ref.

Branch on code, not on title

Stable contract:

if problem["code"] == "COOPERATIVE_NOT_FOUND":
handle_not_found(problem["detail"])

Fragile contract:

if problem["title"] == "Cooperative not found":  # don't — wording may change
...

type and code are equivalently stable; pick one. The kebab-case slug after /errors/ in type corresponds to the lowercase form of code.