Errors
API errors follow standard HTTP status codes. Learn the error response format and how to resolve the most common errors.
Error responses share one body shape: an apiClientMessages array. Every
message carries a category and a machine-readable code; the
remaining fields add human-readable context:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"apiClientMessages": [
{
"category": "ERROR",
"code": "FORMAT_ERROR",
"path": "/validTo",
"text": "Value of validTo is not a valid future date."
}
]
}
| Field | Description |
|---|---|
| apiClientMessages | Array of message blocks — a single response can report several issues. Each block carries the fields below. |
| category | ERROR or WARNING. Required. |
| code | Machine-readable message code, for example FORMAT_ERROR. Required — branch your error handling on this field, not on the human-readable text. |
| path | Path to the request attribute the message refers to, for example /validTo. |
| text | Human-readable explanation of the message, up to 500 characters. |
The exact statuses and codes each operation can return are documented per endpoint in the API reference.
Common errors
The errors you are most likely to meet while integrating:
| Status | Code | Description |
|---|---|---|
| 400 | FORMAT_ERROR |
The request body or a header fails validation — a required attribute is missing, a value has the wrong type, or a date is malformed. Read detail and instance, fix the flagged attribute, and resend. |
| 400 | CONSENT_UNKNOWN |
The Consent-ID header references a consent that does not exist. Pass the consentId returned when the consent was created, without modification. |
| 401 | CERTIFICATE_INVALIDCERTIFICATE_EXPIRED |
The QWAC presented during the TLS handshake was rejected or has expired. Verify the certificate is a valid QWAC with the AISP or PISP role you are using, and renew it if expired. The sandbox also accepts test certificates. |
| 401 | SIGNATURE_INVALIDSIGNATURE_MISSING |
The x-jws-signature header is absent or does not verify against the request. Work through the signing failures checklist below. |
| 401 | CONSENT_INVALIDCONSENT_EXPIRED |
The consent is not in the valid status — the customer has not authorised it yet, revoked it, or its validTo date has passed. Check the consent status endpoint; if the consent expired or was revoked, create a new one and send the customer through authorisation again. |
| 401 | PSU_CREDENTIALS_INVALID |
The customer login failed during authorisation. In the sandbox, use the credentials from the Test credentials page. |
| 403 | SERVICE_BLOCKED |
The requested service is not enabled for this provider. Confirm the endpoint is listed in the API reference; contact support if it is. |
| 404 | RESOURCE_UNKNOWN |
The resource identifier or the request path is wrong. Check the identifier and the full path, including the provider code and the /api/v2 segment. |
| 405 | SERVICE_INVALID |
The HTTP method is not supported on this path. Check the method for the operation in the API reference. |
| 409 | STATUS_INVALID |
The action is not allowed in the current status of the resource — for example, deleting a payment that has already been executed. Fetch the current status first and only perform actions valid for it. |
Signing failures
Most first-week integration errors are SIGNATURE_INVALID responses caused by a
mismatch between the signed request and the request actually sent. Check, in order:
- The
Digestheader is the SHA-256 hash of the exact body bytes you transmit — compute it after resolving any placeholders, and recompute it after every body change. - An empty body still needs a Digest — the hash of an empty string.
- The headers listed in the protected header’s
sigD.parsare signed with exactly the values sent — any header changed after signing invalidates the signature. - The signature is computed with the QSealC private key and the algorithm declared in
alg. - The Postman collection from the Test credentials page builds both headers in its pre-request scripts — use it as a working reference for your own implementation.
How the two headers are built, end to end — the signature covers the request headers listed in
sigD.pars, and the body is covered indirectly through the signed Digest:
# 1. Digest — hash the exact body bytes you will send
body = resolve_placeholders(raw_body)
body = trim(replace(body, CRLF, LF))
digest = "SHA-256=" + base64(sha256(body)) # empty body: hash of ""
# 2. Headers covered by the signature
pars = ["x-request-id", "digest"] # always signed
for h in ["api-contract-id", "psu-id",
"psu-corporate-id", "client-redirect-uri"]:
if request has header h: pars.push(h)
# 3. Protected header
protected = {
"typ": "JOSE",
"alg": "RS256",
"b64": false, # payload is not base64-encoded
"crit": ["b64", "sigT", "sigD"],
"x5c": [base64_der(qsealc_cert)], # PEM armour stripped
"sigT": utc_now("YYYY-MM-DDThh:mm:ssZ"), # no milliseconds
"sigD": { "pars": pars,
"mId": "http://uri.etsi.org/19182/HttpHeaders" },
"aud": method + " " + path_from_v2
# e.g. "POST /v2/consents/account-access"
}
# 4. Payload — signed headers as "name: value" lines, pars order
payload = join(pars.map(h => h + ": " + header_value(h)), "\n")
# 5. Sign and assemble (detached JWS: empty middle segment)
signing_input = base64url(json(protected)) + "." + payload
signature = base64url(
rsa_sha256_sign(signing_input, qsealc_private_key))
x-jws-signature = base64url(json(protected)) + ".." + signature
The two headers then look like this on the wire:
Digest: SHA-256=hl1/Eps8BEQW58FJhDApwJXjGY4nr1ArGDHIT25vq6A=
x-jws-signature: eyJiNjQiOmZhbHNlLCJ4NXQjUzI1NiI6ImR5dFBwU2tKWXpoVGRQWFNXUDdqaFhnRzRrQ09XSVdHaWVzZHprdk5MelkiLCJjcml0IjpbInNpZ1QiLCJzaWdEIiwiYjY0Il0sInNpZ1QiOiIyMDIwLTEwLTI2VDExOjI2OjU3WiIsInNpZ0QiOnsicGFycyI6WyJ4LXJlcXVlc3QtaWQiLCJkaWdlc3QiXSwibUlkIjoiaHR0cDovL3VyaS5ldHNpLm9yZy8xOTE4Mi9IdHRwSGVhZGVycyJ9LCJhbGciOiJSUzI1NiJ9..NuGglWBtHcXavob2ZmW-PoSpMmhJS9U6z8zPqHFoE97vHI3z8wtaVdbwsj5WzgPjtVWUUnk8cxP4JjLXdwzwhhHB2uJ5FI4ZZGdSQQw-9pXPUx3cCJSzHmrgkJ-di-A4
Getting help
If a request keeps failing with a valid certificate, email
compliance@saltedge.com
and include the X-Request-ID of the failing call and the full error response.