Skip to content

Errors

Every error rypt returns is a JSON object with a code, a message and the request's id. Branch on the code, in the error field. The message is for people: several messages carry details of the request, such as a field name, a limit, an address or a time, so do not parse them.

{
  "error": "unauthorized",
  "message": "missing or invalid credentials",
  "request_id": "orders-7f3a9c"
}
Field Type Value
error string The error code, one of those in every error.
message string A description of the error, for people.
request_id string The request's id, the same value as its x-request-id header.
cap integer Only on 429 ops_cap_reached: the monthly cap, 10000.
resets_at string Only on 429 ops_cap_reached: when the count starts again, as an RFC 3339 timestamp in UTC.

Every other error has exactly the first three fields. The response's content type is application/json. The response at the cap shows a full ops_cap_reached body.

A 500 internal_error always has the message internal error. rypt logs its cause under the request id and never returns it.

Every response, success or error, carries an x-request-id header, and an error body repeats it as request_id. The audit log stores it in the row a request writes. When you report a problem to support@rypt.dev, include the id, and never an API key.

You can choose the id. If your request has an x-request-id header of 1 to 128 characters, each an ASCII letter or digit, _, ., : or -, rypt uses it as the request's id. Any other value is dropped, and rypt generates a UUID instead. rypt does not check that your ids are unique.

curl -sS -i https://api.rypt.dev/v1/keys -H "x-request-id: orders-7f3a9c"

This request has no Authorization header, so rypt refuses it with 401 unauthorized and the body shown under the error body, and the response's x-request-id header is orders-7f3a9c.

When a request has more than one problem, the first check to fail decides the answer; how a request is decided gives the order. "Dashboard:" marks codes that only a dashboard session can get. Which errors leave a row in a key's audit log is set out under what is recorded, and for 403 refusals under which refusals are recorded.

Status Code When Retry?
400 invalid_request The body is not JSON or has the wrong shape (see routing and body errors), or plaintext on encrypt decodes to 0 bytes. No
400 invalid_base64 A binary field is not standard base64 with = padding. The message names the field. No
400 invalid_dek_length The dek sent to wrap does not decode to 16, 24 or 32 bytes. No
400 invalid_ciphertext decrypt, unwrap or rewrap cannot open the value: a different aad, a changed value, another key's value on a Free or Extra wrapped key, or a key version that is no longer enabled. See when the aad does not match and rotation. No
400 invalid_name A name is empty, only whitespace, over 128 characters, or has a control character. No
400 invalid_tier tier on POST /v1/keys is not exactly free, wrapped, software or hsm. No
400 invalid_container container_id names no container the request may use. An API key may use only its own. No
400 invalid_query since, until or limit on GET /v1/keys/{id}/audit is malformed or out of range, or the query string of that route or of GET /v1/usage cannot be read, for example because a parameter is repeated. No
400 invalid_period period on GET /v1/usage is present but is not a month written YYYY-MM. No
400 invalid_allowed_actions Dashboard: an API key's actions include a name that is not an action, or are null on a change. No
400 invalid_key_ids Dashboard: an API key's key list is empty, names over 100 keys, or names something that is not a live key in the API key's container. No
400 invalid_firewall Dashboard: firewall_id is not a firewall in the API key's container. No
400 invalid_allowed_ips Dashboard: a firewall's list is empty, names over 64 networks, or has an entry rypt does not accept. See firewalls. No
401 unauthorized The credential is missing, malformed, unknown, revoked or wrong, or a dashboard session did not verify (for example, it expired). Every cause gets the same message. No
402 billing_required Creating a paid-tier key, or encrypt, wrap or rewrap on one, while the tenant's billing is not in good standing. See billing refusals. After billing is fixed
403 api_key_required A dashboard session called encrypt, decrypt, wrap, unwrap or rewrap, which take an API key only. No
403 session_required An API key called a route that only a dashboard session may use. See dashboard sessions. No
403 ip_not_allowed The API key is held to a firewall, and the request came from outside its networks. The message names the address rypt saw. Not from that address
403 key_not_allowed The API key has a key list and the key in the path is not on it, or it tried to create a key, which an API key with a key list cannot do whatever its actions. No
403 action_not_allowed On one of the ten routes that check actions, the API key was not given the action the route needs. What actions do not restrict lists the routes and their actions. No
403 no_tenant Dashboard: the signed-in user belongs to no tenant. No
404 not_found The path is not a route, or its id is not a UUID or names nothing the caller can reach: unknown, destroyed (its audit log stays readable), another tenant's, or, for an API key, in another container. No
405 method_not_allowed The path is a route, but not with this method. No
409 key_pending_deletion The key is pending deletion, so it refuses the five operations, rotation and renaming. The message says when the key will be destroyed, or, once that time has passed, that it can no longer be restored. After a restore, while one is possible
409 key_exists Another live key in the container, pending deletion included, has that name. No
409 free_key_exists The tenant already has a live Free (free) key; one pending deletion counts. See one Free key per tenant. No
409 not_pending_deletion POST /v1/keys/{id}/restore on a key that is not pending deletion. No
409 restore_window_closed A restore after the key's destroy_after has passed. The key can no longer be restored. No
409 container_exists Dashboard: another live container of the tenant has that name. No
409 container_is_default Dashboard: deleting the default container. No
409 container_not_empty Dashboard: deleting a container that still holds keys (pending deletion included), live API keys or firewalls. No
409 firewall_exists Dashboard: another live firewall in the container has that name. No
409 firewall_in_use Dashboard: deleting a firewall that live API keys are held to. No
409 billing_active Dashboard: starting a checkout while the tenant has a subscription open. No
409 billing_not_set_up Dashboard: opening the billing portal before billing was set up. No
413 payload_too_large The body is over 128 KiB, or a field is over its size limit after decoding. See sizes. No
415 unsupported_media_type A route that takes a JSON body got a request without a JSON Content-Type. No
429 rate_limited The API key, or the dashboard user, has used up its rate limit. See the rate limit. Yes, after a delay
429 ops_cap_reached A Free key has used its 10,000 operations this UTC month, so all five operations are refused. See the Free tier's monthly cap. Not before resets_at
500 internal_error rypt failed, or its database, Cloud KMS or Stripe did. A Cloud KMS outage is a 500, not a 503. See retrying
503 unavailable rypt was too busy to start the request in time (too many API key checks, key changes or new API keys in flight), or got no answer from something it needed: the check of the API key's actions, the dashboard's sign-in provider, or Stripe. The message says which. Yes, after a delay

These answer a request rypt cannot route, or a body it cannot read. The four body messages are the text of the web framework rypt is built on, and the two 400 messages end with its description of the problem:

Request Answer message
A path that is not a route 404 not_found not found
A route with a method it does not take 405 method_not_allowed method not allowed
A JSON body without a JSON content type 415 unsupported_media_type Expected request with `Content-Type: application/json`
A body over 128 KiB 413 payload_too_large Failed to buffer the request body: length limit exceeded
A body that is empty or not valid JSON 400 invalid_request Failed to parse the request body as JSON: , then the description
JSON of the wrong shape, such as a missing field or a number where a string belongs 400 invalid_request Failed to deserialize the JSON body into the target type: , then the description

For example, {} sent to encrypt gets Failed to deserialize the JSON body into the target type: missing field `plaintext` at line 1 column 2, and {"plaintext":1} gets Failed to deserialize the JSON body into the target type: plaintext: invalid type: integer `1`, expected a string at line 1 column 14.

  • A path that is not a route answers 404 not_found, and a route with the wrong method 405 method_not_allowed, whatever the credentials.
  • The content type is checked before the body is read, so a body sent without it gets 415 whatever its size or contents. application/json; charset=utf-8, and application/ types ending in +json such as application/merge-patch+json, are accepted; text/json is not.
  • A route that takes a JSON body needs one even when every field is optional: to have wrap generate a data key, send {}. With no body, the request gets 415 without a content type, and 400 with one, with the message Failed to parse the request body as JSON: EOF while parsing a value at line 1 column 0.
  • JSON of the wrong shape is 400, not 422.
  • Fields rypt does not know are ignored, so a misspelled optional field is not an error: see a misspelled field means no aad. Only PATCH /v1/api-keys/{id} and PATCH /v1/firewalls/{id} refuse unknown fields, with 400 invalid_request.
  • On a route with a key id in its path, the body is read after authentication and after the checks of the path, the API key's key list and, where they apply, its actions. A refused request gets its 403 even with a malformed body, and an unknown id with a malformed body gets 400, not 404: see how a request is decided.
  • POST /v1/keys reads its body first, before the API key's key list and actions are checked. A body it cannot read gets 400 invalid_request, 413 payload_too_large or 415 unsupported_media_type even from an API key that would be refused. With a well-formed body, 403 key_not_allowed or 403 action_not_allowed comes before name and tier are checked: see how a request is decided.

rypt sends no retry hints: no response has a Retry-After header or X-RateLimit-* headers. Choose your own delays, backing off exponentially with jitter. rypt has no idempotency keys.

  • 429 rate_limited and 503 unavailable are the codes to retry, after a delay.
  • 429 ops_cap_reached is refused again on every retry until resets_at. See what to do at the cap.
  • Every other 4xx repeats until something changes: the request, the API key's settings (a change applies from its next request, see when changes take effect), the tenant's billing, or the key's state.