API Overview
Clients communicate with ConfigHub Cloud via a HTTP REST API. This page gives you an overview of how the API is structured. You can find the complete reference for the API in the OpenAPI Spec at https://hub.confighub.com/api/docs.
Authentication
For endpoints that require authentication, the API requires a valid JWT bearer token in the Authorization header of the HTTP request. There is currently no official documentation for how to obtain a token, but you can read the source code of the CLI to see how it obtains a token.
Writing clients
We generate Go and Typescript client libraries from our OpenAPI spec. The Go client is used in the CLI, cub, which you can use as an example client. You can find the client libraries and cub source code in the https://github.com/confighub/sdk repo.
All details for writing clients are encoded in the OpenAPI Spec at https://hub.confighub.com/api/docs and will not be repeated here. Please use the spec to generate clients or refer to the spec for details when hand writing clients.
JSON format conventions
JSON structures use PascalCase for field names and string constant names. For example:
{
"OrganizationID": "123",
"Slug": "a-record",
"CreatedAt": "..."
}
URL Structure
The URL structure of the REST API reflects the nested collections topology of entities in ConfigHub. Collection names are singular nouns rather than plural. IDs are used as identifiers in URLs. The organization comes from the authentication context, since one logs in as a member of a specific organization.
This will fetch a unit:
GET /api/space/55ec4079-5b96-44f6-9a02-d6bf74286e1e/unit/9872639e-373e-4839-8566-51a928bb8f3d
ConfigHub checks that the URL pattern matches the current state of an entity. For example, in the URL above, if the Unit with UUID 9872639e-373e-4839-8566-51a928bb8f3d exists but is not in the space with UUID 55ec4079-5b96-44f6-9a02-d6bf74286e1e, then the request returns 404 not found even though the Unit does exist.
ConfigHub will also check access permissions and return 403 forbidden if an operation is not permitted by the currently authenticated principal.
A list is the exception: it leaves out the entities you may not view rather than failing, so a list can be empty because nothing in it is visible to you. A list under a Space or Unit that does not exist, such as GET /api/space/{space_id}/unit with an unknown Space ID, returns 404, as a GET of that Space would.
Errors
Every error response has the same body. Error is the error as data, and message is the same error as one string:
{
"message": "the Slug is required\nDetails:\n failed to create entity\nMetadata:\n Slug: the Slug is required\n",
"Error": {
"Message": "the Slug is required",
"Type": "UserInput",
"Status": 400,
"Details": ["failed to create entity"],
"ErrorMetadata": {
"Items": [{"Item": "Slug", "Description": "the Slug is required"}]
}
}
}
Error.Messageis the primary message, andError.Detailsadds context from the layers the error passed through.Error.ErrorMetadatanames the entity the error is about, byEntityType,EntityIDandEntitySlug, when there is one, and lists inItemsthe fields or entities each problem concerns.Error.Statusis the HTTP status of the response.Error.Typeis the category of the error, as a code. Branch on it rather than on the text of the message, which may change. It plays the role of thereasonof a KubernetesStatus, and several of the codes are the same.messageis the message followed by the details and items, as one string. It is the only lowercase key in the API, kept for clients written beforeErrorexisted. New clients should readError.
The Type codes, and the status each one is returned with:
| Type | Status | Meaning |
|---|---|---|
UserInput |
400 | The request is malformed: a body that is not valid JSON, a field of the wrong type, an invalid where expression, an invalid value. Also the type of a 405 or 415. |
Validation |
400 | The request violates a constraint on the data, such as a reference to an entity that does not exist. |
Data |
400 | A value is out of range or in the wrong format for where it is stored. |
Authentication |
401 | The request has no valid token. |
Permission |
403 | The caller may not perform the operation. |
NotFound |
404 | The entity, or the Space or Unit it is listed under, does not exist. |
Conflict |
409 | The request conflicts with the entity's current state: another write came first, so its Version, or the If-Match of a configuration write, is not the stored one; or the entity cannot be deleted while something still uses it. |
AlreadyExists |
409 | An entity with the same identity, such as the same Slug in the Space, already exists. |
PreconditionFailed |
412 | What the request was prepared against has changed, such as a promotion whose plan no longer matches the one previewed; preview it again. |
Unprocessable |
422 | The request is well formed, but the operation cannot be carried out, such as a function invocation whose arguments do not convert, or a Release publish its gates refuse. |
Locked |
423 | The entity is held by an operation that cannot be interrupted, such as a Unit with an in-flight action that cannot be canceled. |
TooManyRequests |
429 | Too many requests are outstanding, such as function invocations on one worker; retry shortly. |
Unexpected |
500 | The server failed. Retrying may succeed; if it does not, report the error with the response's X-Request-Id. |
ServiceUnavailable |
503 | The server cannot take requests at the moment. |
GatewayTimeout |
504 | A service the server depends on did not answer in time. |
Two of the types share 409, so a client that acts on a 409 checks Type. A Conflict from an update is the optimistic concurrency check, like a Kubernetes resourceVersion conflict: read the entity again, and reapply the change to what you read. An AlreadyExists will not go away on a retry; choose another Slug.
A request with a method the path does not have, such as PUT /api/space, returns 405, with an Allow header listing the methods it does have.
Errors in other responses. Some operations report a failure in their own response type, because what you need to act on is more than the error, and put the error in an Error field of it:
- Bulk operations return 200 when every entity succeeds, and 207 when some fail, with an
Errorin the response for each one that failed. When every entity fails with the same error, the operation returns that one error, with its status. - Promotion that a Stage's gates refuse returns 409 with the
PromoteResult, whoseErrorlists the message of each gate that does not hold, and whoseStagescarry the result of every gate evaluated. - A Release publish that its gates refuse returns 422 with the
ReleasePublishResponse, whoseGatesare the gates evaluated and whoseErroris the refusal.
Warnings
A field in a request body that the entity or request type does not have is ignored, so that a client written for an older or newer version of the API keeps working. So that a misspelled field does not go unnoticed, the response names each such field in a Warning header, in the form RFC 7234 gives it and Kubernetes uses for the same purpose:
Warning: 299 - "unknown field \"Lables\""
A field nested in another is named by its path, such as Permissions.View.Bogus. cub prints each warning a command receives to stderr:
Warning: unknown field "Lables"
Configuration data
Configuration data is not a field of the entity that owns it. A Unit, a Revision, and a Release each expose their bulk fields as subresources, so that fetching or listing the entity does not carry a copy of every configuration:
GET /api/space/{space_id}/unit/{unit_id}/data
PUT /api/space/{space_id}/unit/{unit_id}/data
GET /api/space/{space_id}/unit/{unit_id}/mutation_sources
GET /api/space/{space_id}/unit/{unit_id}/revision/{revision_id}/data
GET /api/space/{space_id}/unit/{unit_id}/revision/{revision_id}/mutation_sources
GET /api/space/{space_id}/release/{release_id}/data
The request and response bodies of the Unit and Revision data endpoints are the configuration itself, as text, rather than a JSON object with the configuration inside it. A GET returns the DataHash as an ETag, and honors If-None-Match with a 304; a PUT honors If-Match on that same hash, so a write can be made conditional on nobody having written first. PUT .../data is the only way configuration reaches a Unit other than a clone, which copies it server-side; it routes through the normal update path, so it creates a Revision and runs gates, triggers, and link resolution like any other change. It accepts the update query parameters that do not name another source for the data, such as last_change_description and change_set_id. A Release's data is the gzip tarball it published, and its ETag is the Release's Digest.
A write to a Unit — POST /api/space/{space_id}/unit, PUT or PATCH on one, and the bulk forms — answers with UnitCreateOrUpdateResponse rather than the Unit itself. The Unit is in its Unit field. That is also where the configuration an operation produced can be returned, which is the only way to see the result of a dry run: a dry run stores nothing, so there is no data endpoint to read it back from. Ask for it with include:
PUT /api/space/{space_id}/unit/{unit_id}?dry_run=true&upgrade=true&include=ConfigData,MutationSources
ConfigData is the configuration the operation produced and MutationSources is what set each value in it. Both are returned only when named, on a real write as well as a dry run; naming something that is neither of these nor an expandable field is a 400.
Because these fields are not part of the entity, they cannot be named in a select clause — select=Data is a 400. What the entity does carry is DataHash, the hash of the configuration, and DataSize, its length in bytes.
Reading one Unit at a time does not scale to a whole Space, so there are org-scoped bulk counterparts that take a where clause and answer for every entity it selects, each with its DataHash and DataSize:
GET /api/unit_data
GET /api/revision_data
GET /api/unit_mutation_sources
GET /api/revision_mutation_sources