Skip to main content

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

StatusMeaningCarries detailsRetry
400The request was rejected. On a validation failure, every offending field is listed.On Embed and Record writesNo, with one exception below
401The access token is missing, expired, or invalid.NoAfter obtaining a new token
403The credential is valid but not permitted to perform this operation.NoNo
404The job or signature does not exist, was registered by another partner, or has no Record attached.NoNo
409The signature is revoked, so its Record can no longer be modified.NoNo
412The If-Match precondition did not match the signature's current revision. The Record was not modified.NoOnce, after re-reading the Record
429The request exceeded the rate limit. See Rate Limiting.NoYes, after backing off
500An internal failure.NoOnce; if it persists, escalate with the request_id.
503A dependency is temporarily unavailable.NoYes, 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.codeMeaning
image_already_protectedThe image already carries a SASHA Signature.
image_load_failedThe media is not a valid image, or is in a format SASHA does not process.
image_too_largeThe image exceeds the size SASHA processes. See Supported Formats.
image_too_smallThe image is below the minimum size.
internalAn 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"
}
]
}
FieldPresenceMeaning
messagealwaysHuman-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_idwhen availableMirrors the SASHA-Request-ID response header.
detailsvalidation failures onlyEvery 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.

ReasonMeaning
REQUIREDA field the request cannot omit was not provided.
INVALID_TYPEThe value has the wrong JSON type for the field.
INVALID_FORMATThe 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_SCHEMEA URL uses a scheme the field does not accept. Only https is accepted.
FORBIDDEN_USERINFOA URL carries userinfo (user:password@), which is never accepted.
FORBIDDEN_HOSTA URL points at a host the field does not accept, such as an IP literal or localhost.
TOO_LONGThe value is longer than the field allows.
TOO_MANYThe list or map holds more entries than the field allows.
INVALID_CHARACTERSThe value contains characters the field does not accept, such as control characters.
UNKNOWN_VALUEThe value is not one of the values the field defines, e.g. an unknown share_state.
NOT_ALLOWED_HEREThe field is well-formed but not allowed in this combination, e.g. a distribution field on a private Record.
EXACTLY_ONE_REQUIREDExactly one of a group of mutually exclusive properties must be set, e.g. the typed value of a custom_fields entry.
UNKNOWN_FIELDThe body carries a field the endpoint does not define — commonly a misspelling, or a camelCase spelling of a snake_case field.
EMPTYThe value is present but empty where a value is required.
INVALID_PATTERNA 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.