Error Handling
A failing Partner API request returns a JSON body carrying a message. A validation failure adds a details array naming every field that was rejected, so a malformed request can be corrected in one pass rather than one field at a time.
Two parts of an error are contract and safe to branch on: the HTTP status code, and the reason on each entry in details. Everything else, including all human-readable text, may change between releases.
For the code that acts on this, see Handle API Errors.
Status codes
| Status | Meaning | Carries details | Retry |
|---|---|---|---|
400 | The request was rejected. On a validation failure, every offending field is listed. | On Embed and Record writes | No, with one exception below |
401 | The access token is missing, expired, or invalid. | No | After obtaining a new token |
403 | The credential is valid but not permitted to perform this operation. | No | No |
404 | The job or signature does not exist, was registered by another partner, or has no Record attached. | No | No |
409 | The signature is revoked, so its Record can no longer be modified. | No | No |
412 | The If-Match precondition did not match the signature's current revision. The Record was not modified. | No | Once, after re-reading the Record |
429 | The request exceeded the rate limit. See Rate Limiting. | No | Yes, after backing off |
500 | An internal failure. | No | Once; if it persists, escalate with the request_id. |
503 | A dependency is temporarily unavailable. | No | Yes, after backing off |
Only 400 carries details, and only where the endpoint validates a request body: Embed, PUT Record and PATCH Record. Every other failure carries message alone. 401 and 403 return a body of message only, with no request_id — read the SASHA-Request-ID header instead.
The exception to "do not retry a 400": an Embed or Lookup submitted with a media_url returns 400 when SASHA cannot fetch that URL, which is a failure of the remote host rather than of the request. That one is worth a single retry; every validation failure is deterministic and is not.
412 does not always mean a stale ETag. A Record write on a revoked signature also reports 412 when it carries an If-Match header, so a re-read-and-retry loop on 412 will not converge. Retry once; if the second attempt fails the same way, treat the signature as revoked and stop.
404 covers an unknown signature, one registered by a different partner, and one that simply has no Record attached. The first two are deliberately indistinguishable: separating them would tell a caller which SignatureIDs exist.
Every response, successful or not, carries a SASHA-Request-ID header identifying that request. It joins a partner's call to the server-side logs, and it is the first thing SASHA support asks for.
Jobs fail after the request succeeds
Embed and Lookup are asynchronous. A 200 on the submit means the job was accepted, not that it worked: the media is processed afterwards, and the failure arrives on the completed job as status: "failed" with an error object, either on a poll of GET /jobs/{job_id} or on the callback. An integration that only handles HTTP status codes will treat a failed job as a success.
error.code | Meaning |
|---|---|
image_already_protected | The image already carries a SASHA Signature. |
image_load_failed | The media is not a valid image, or is in a format SASHA does not process. |
image_too_large | The image exceeds the size SASHA processes. See Supported Formats. |
image_too_small | The image is below the minimum size. |
internal | An internal failure. Retry the job; if it persists, escalate with the request_id of the submit. |
Only internal is worth retrying. The other four are properties of the media, so the same media fails the same way.
A media_url that cannot be fetched does not reach this stage: the download happens during the submit, so it fails the request with 400 rather than the job.
Error response shape
A failing request to the Signature, Job or Record endpoints returns an ErrorResponse:
{
"message": "purchase_url must use the https: protocol (+2 more)",
"request_id": "8f14e45f-ceea-467a-9f0b-2b3a2c5a7d61",
"details": [
{
"field": "purchase_url",
"reason": "UNSUPPORTED_SCHEME",
"description": "purchase_url must use the https: protocol"
}
]
}
| Field | Presence | Meaning |
|---|---|---|
message | always | Human-readable summary. On a validation failure it is the first details entry's description, suffixed with (+N more) when the request has more than one violation. |
request_id | when available | Mirrors the SASHA-Request-ID response header. |
details | validation failures only | Every field that was rejected, up to 20 entries. |
The /oauth/token endpoint is the exception: it returns the OAuth 2.0 error shape (error, error_description, error_uri) defined by RFC 6749. See Authentication.
Field violations
Each details entry names one rejected field:
field— the path of the offending field, in request-body naming. Array entries are indexed and map entries are keyed:purchase_url,allowed_publish_urls[2],custom_fields["asset_id"].string_value.reason— a machine-readable cause. This is the value to branch on.description— a human-readable explanation. It never echoes the submitted value, so it is safe to log and to surface in a partner's own error reporting.
A request with several problems reports all of them at once:
{
"message": "share_state must be one of: private, unrestricted, restricted (+2 more)",
"request_id": "2c3f9b7d-5a41-4a53-8f2e-9b7a1c0d4e62",
"details": [
{
"field": "share_state",
"reason": "UNKNOWN_VALUE",
"description": "share_state must be one of: private, unrestricted, restricted"
},
{
"field": "allowed_publish_urls[1]",
"reason": "INVALID_PATTERN",
"description": "allowed_publish_urls[1]: wildcard is only allowed as the leftmost label"
},
{
"field": "purchaseUrl",
"reason": "UNKNOWN_FIELD",
"description": "purchaseUrl is not a field of this request; did you mean purchase_url? (this API uses snake_case field names)"
}
]
}
Reason catalogue
The FieldViolation schema in the REST API reference is the authoritative list: it is generated from the API specification and gains new values as they are introduced. This table explains what each current value means, and is the copy that goes stale first.
| Reason | Meaning |
|---|---|
REQUIRED | A field the request cannot omit was not provided. |
INVALID_TYPE | The value has the wrong JSON type for the field. |
INVALID_FORMAT | The value has the right type but is not well-formed, e.g. an unparsable URL or timestamp, or a fixed-length value of the wrong length. |
UNSUPPORTED_SCHEME | A URL uses a scheme the field does not accept. Only https is accepted. |
FORBIDDEN_USERINFO | A URL carries userinfo (user:password@), which is never accepted. |
FORBIDDEN_HOST | A URL points at a host the field does not accept, such as an IP literal or localhost. |
TOO_LONG | The value is longer than the field allows. |
TOO_MANY | The list or map holds more entries than the field allows. |
INVALID_CHARACTERS | The value contains characters the field does not accept, such as control characters. |
UNKNOWN_VALUE | The value is not one of the values the field defines, e.g. an unknown share_state. |
NOT_ALLOWED_HERE | The field is well-formed but not allowed in this combination, e.g. a distribution field on a private Record. |
EXACTLY_ONE_REQUIRED | Exactly one of a group of mutually exclusive properties must be set, e.g. the typed value of a custom_fields entry. |
UNKNOWN_FIELD | The body carries a field the endpoint does not define — commonly a misspelling, or a camelCase spelling of a snake_case field. |
EMPTY | The value is present but empty where a value is required. |
INVALID_PATTERN | A URL pattern is malformed and cannot be interpreted. |
The set is closed but may be extended, so an unrecognised reason means a value newer than the integration, not a malformed response. Treating it as a generic validation failure keeps an integration working across releases; failing hard on it does not.
Field naming
The REST request body uses snake_case field names, matching the API specification exactly. Generated gRPC clients typically present the same fields camelCased — purchaseUrl in gRPC is purchase_url in JSON. Sending the camelCase spelling to the REST API produces an UNKNOWN_FIELD violation naming the correct spelling.
Unknown fields on a Record write are rejected rather than ignored. This is deliberate: PUT /signature/{signature_id}/record replaces the whole Record, so a misspelled field name would otherwise be silently dropped, clearing the value it was meant to set.