Skip to content

Introduction

Keystash's APIs belong to the Representational State Transfer (REST) category. They allow you to perform 'RESTful' operations such as reading, modifying, adding or deleting data from your SSH key management system.

Note

Every API request must be authenticated. We recommend using a Personal Access Token — see the Authentication page for details.

What API commands are used by Keystash?

Keystash APIs are plain JSON over HTTP and use the following HTTP verbs:

Command Purpose
POST Create an object
GET Fetch one or more objects
PUT Set an objects state or replaces an object
PATCH Update an object
DELETE Remove an object

Note

All API requests should hit the secured HTTPS endpoint.

Base Endpoint URL

The base REST endpoint URL is:

https://app.keystash.io/api/v1/

All the of the Resources and Methods prefix this base URL.

Example

Resource: Users

Method: List Users

Final URL: https://app.keystash.io/api/v1/users/list

Rate Limit

Keystash implements a rate limit on connections to the API. The API allows:

5 connections/second.

Should you you reach the API limit you will receive the following error response body. The Retry-After value in the response header will tell you how long you need to wait before you can send another API request.

HTTP/1.1 429

X-Ratelimit-Remaining: 0
X-RateLimit-Limit: 1
X-RateLimit-Reset: {datetime}
Retry-After: 10

Header Name Description
X-RateLimit-Remaining The number of requests remaining in the current rate limit window.
X-RateLimit-Limit The number of requests used in the current rate limit window.
X-RateLimit-Reset The date and time at which point you can resume submitting requests.
Retry-After The number in seconds that you will have to wait to fire
your next API request.

Filtering Lists

Several of the list endpoints accept query parameters that narrow the results before they are returned to you. The filtering is done in the database, so a narrow request is quicker and cheaper than pulling everything back and sorting it out yourself. The endpoints that accept filters are:

Each resource page documents the parameters that its endpoint accepts. The rules on this page apply to all of them.

Filters are validated strictly. A value that fails its rule, a parameter the endpoint does not accept, or a malformed date returns a 400 BadArgument before any query is run. The response names every parameter at fault at once, so three mistakes in a request are reported together rather than one at a time over three round trips.

Four properties keep that strictness comfortable to work with:

  • A parameter you do not send is never validated. A request with no query string at all is a perfectly normal first page.
  • A single value works where an array is expected. ?statuses=Connected behaves exactly as ?statuses[]=Connected does.
  • Fixed vocabularies are case-insensitive. ?statuses[]=connected is accepted and matched against the Connected status.
  • A boolean set to false is a filter, not an absent one. ?two_factor_enabled=false returns exactly the Server Groups that have the setting switched off.

A limit above the maximum, an offset past the end of the results and a valid filter that happens to match nothing are all handled normally rather than rejected. See Paging Through Lists for what each of those returns.

Each entry in the details array of a rejection describes one parameter that was refused:

Key Always Present Meaning
field yes The query parameter at fault, named exactly as you sent it.
message yes The rule that the value broke.
received no The offending value, truncated to 64 characters. For an array, the first member that failed. Absent when there was no value to echo, such as the missing half of a date pair.
allowed_values no Present when the parameter has a fixed vocabulary. On an unknown parameter it lists every parameter that the endpoint accepts.
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8

{
  "error": {
    "code": "BadArgument",
    "message": "One or more query parameters were invalid.",
    "details": [
      {
        "field": "statuses",
        "message": "Unrecognised status label. Supply one or more of the allowed values.",
        "received": "Banana",
        "allowed_values": ["Connected", "Syncing", "Warning", "Upgrading", "Disconnected"]
      }
    ]
  }
}

Where the parameter name itself is wrong, the message carries a suggestion if the slip is a difference of case, or a singular where a plural was expected. Sending ?provider=Linode to List Servers returns "Unknown query parameter. Did you mean 'providers'?".

Note

Dates are matched against the exact format YYYY-MM-DD HH:mm:ss and are read as UTC. A date without a time, an ISO 8601 value carrying a T or a time zone offset, or a differently separated value is rejected. Send 2026-06-08 23:59:59 rather than 2026-06-08, because a date on its own can only mean midnight, so an end_date of 2026-06-08 would drop the whole day you asked for.

Tip

Array parameters accept both ?statuses[]=Active and ?statuses=Active. Use the bracketed form whenever you are supplying more than one value, repeating the parameter once per value: ?statuses[]=Active&statuses[]=Ended.

Paging Through Lists

The same two parameters page every list that accepts filters:

Parameter Default Accepted Behaviour
limit 250 1250 The number of rows to return. The default and the maximum are the same number, so limit is a reduction knob: you can ask for fewer rows but never for more. A value above 250 is reduced to 250.
offset 0 0 or greater The row offset into the matching set.

The response body is always a plain JSON array of records. The paging metadata travels in the response headers instead:

Header Meaning
X-Total-Count The number of rows matching all of the filters you supplied, before limit and offset are applied.
X-Limit The effective limit after any reduction, rather than the raw value you sent.
X-Offset The effective offset.

All three headers are exposed through CORS, so browser JavaScript can read them.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Total-Count: 1100
X-Limit: 250
X-Offset: 250

[ ...250 records... ]

To walk a list, read X-Total-Count from the first response and request pages until you have covered it. If you would rather not track a total, keep requesting pages until one comes back with fewer rows than your limit. Both approaches work.

Warning

The API is limited to 5 requests per second, so page sequentially rather than requesting several pages at once. A fleet of 1,100 servers is 5 requests at 250 rows a page, and a company with 50,000 SSH sessions is 200 requests. Where the set is large, narrow it with a filter, such as a date range on SSH Sessions, rather than walking the whole history.

Note

The list endpoints fall into two families and they treat an empty result differently.

Collection lists are List Servers, List SSH Sessions and List Server Groups.

Condition Response
No filters supplied and X-Total-Count is 0 404 NotFound
Filters supplied and nothing matched 200 OK with [] and X-Total-Count: 0
An offset past the end of a non-empty set 200 OK with [] and the true X-Total-Count

Read that 404 as "you have none of these yet" rather than as an error. It is driven by the total, never by the length of the page you asked for.

Membership lists are List Users in Server Group and List User Groups in Server Group.

Condition Response
Nothing to list 200 OK with []
An offset past the end 200 OK with [] and the true X-Total-Count
An unknown id 404 NotFound

Each list has a fixed order, so walking offset = 0, 250, 500 and onwards over a set of data that is not changing underneath you visits every row exactly once. The order is a total one, so even where two records share the same name or hostname their position relative to each other is settled and a page boundary never drops or repeats a row.

Endpoint Order
List Servers hostname ascending
List SSH Sessions started_at descending, newest first
List Server Groups name ascending
List Users in Server Group lastname, then firstname, ascending
List User Groups in Server Group name ascending

Error Responses

Every error the API returns uses the same envelope:

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8

{
  "error": {
    "code": "BadArgument",
    "message": "One or more query parameters were invalid.",
    "details": []
  }
}
Key Meaning
error.code A stable, machine-readable code from the table below. Test against this rather than against the message.
error.message A human-readable summary of what went wrong.
error.details An array carrying the specifics. It is empty where there is nothing to add. On a rejected query parameter it carries one entry per parameter at fault, as described in Filtering Lists.
Code Status When
BadRequest 400 The request itself could not be read.
BadArgument 400 An argument was missing, or present and invalid.
Unauthorized 401 The credentials were rejected.
InvalidPersonalAccessToken 401 The Personal Access Token is unknown, expired or revoked.
InvalidJSONWebToken 401 The JSON Web Token is invalid or has expired.
Forbidden 403 The authenticated user's Role does not allow this path.
NotFound 404 The request succeeded but there was nothing to return.
DataRetrievalError 500 A problem occurred while reading from the database.
DataSaveError 500 A problem occurred while writing to the database.
UnknownSystemError 500 A server-side fault. Retry the request, and log a ticket with us if it persists.

A 403 names the path you were refused, and there is a worked example of one on the Authentication page.

Note

A rejected request costs you nothing. There is no lockout or penalty for a validation failure, so you can correct your parameters and retry straight away, subject only to the rate limit of 5 requests per second.

Dates and Times

All dates and times in the Keystash API are in UTC time. The date and time format used across the platform is ISO 8601 YYYY-MM-DD HH:mm:ss. Example: 2020-02-26 18:30:26