# Certificates

Store your own TLS certificates, including wildcards, and see which certificate Ceryx serves for a domain.

Certificates stored through the API take priority over Let's Encrypt. A wildcard certificate stored for `*.example.com` serves subdomains such as `app.example.com` that have no certificate of their own. Responses never include private keys. See [How certificates work](/docs/concepts/certificates) for the full order in which Ceryx picks a certificate.

## List stored certificates

`GET /api/certificates`

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | Certificates stored through the API. | array of [CertificateListItem](/docs/api/schemas#certificatelistitem) |

### Example

```bash
curl http://ceryx:8080/api/certificates
```

Responds with `200`:

```json
[
  {
    "domain": "*.example.com",
    "meta": {
      "vendor": "Example CA",
      "san": [
        "*.example.com"
      ],
      "expires_at": "2027-02-01T00:00:00Z",
      "issued_at": "2026-11-03T00:00:00Z",
      "stored_at": 1793750400
    }
  }
]
```

## Create or replace a certificate

`POST /api/certificates`

Stores the certificate for `domain`, replacing any existing one.

### Request body

A [CertificateCreate](/docs/api/schemas#certificatecreate) object.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `domain` | string | Yes | Optional; must equal the `domain` in the URL. |
| `certificate` | string | Yes | PEM-encoded certificate. |
| `private_key` | string | Yes | PEM-encoded private key. |
| `chain` | string | No | PEM-encoded intermediate chain. |
| `meta` | [CertificateMeta](/docs/api/schemas#certificatemeta) | No | Free-form metadata. Ceryx adds `issued_at` (when not given) and `stored_at` on every write. |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `201` | The stored certificate (without the private key). | [Certificate](/docs/api/schemas#certificate) |
| `400` | Missing or invalid input. | [Error](/docs/api/schemas#error) |
| `500` | The storage backend failed, or a Ceryx environment variable the request needs has an unusable value (`detail` names it). | [Error](/docs/api/schemas#error) |

### Example

```bash
jq -n \
  --rawfile certificate cert.pem \
  --rawfile private_key key.pem \
  --rawfile chain chain.pem \
  '{domain: "*.example.com", certificate: $certificate, private_key: $private_key, chain: $chain,
    meta: {vendor: "Example CA", san: ["*.example.com"], expires_at: "2027-02-01T00:00:00Z"}}' |
curl -X POST http://ceryx:8080/api/certificates \
  -H 'Content-Type: application/json' \
  -d @-
```

Responds with `201`:

```json
{
  "domain": "*.example.com",
  "has_certificate": true,
  "has_chain": true,
  "meta": {
    "vendor": "Example CA",
    "san": [
      "*.example.com"
    ],
    "expires_at": "2027-02-01T00:00:00Z",
    "issued_at": "2026-11-03T00:00:00Z",
    "stored_at": 1793750400
  }
}
```

## Get the certificate served for a domain

`GET /api/certificates/{domain}`

Returns the certificate Ceryx would serve for `domain`: an exact stored match, then a stored wildcard (`*.parent`), then certificate files from the route settings.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `domain` | string | Certificate domain; may be a wildcard such as `*.example.com`. |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The certificate (without the private key). | [Certificate](/docs/api/schemas#certificate) |
| `404` | No such resource. | [Error](/docs/api/schemas#error) |

### Example

```bash
curl http://ceryx:8080/api/certificates/app.example.com
```

Responds with `200`:

```json
{
  "domain": "app.example.com",
  "has_certificate": true,
  "has_chain": true,
  "meta": {
    "vendor": "Example CA",
    "san": [
      "*.example.com"
    ],
    "expires_at": "2027-02-01T00:00:00Z",
    "issued_at": "2026-11-03T00:00:00Z",
    "stored_at": 1793750400
  }
}
```

## Update a stored certificate

`PUT /api/certificates/{domain}`

Updates the certificate stored for exactly `domain` (no wildcard or file fallback). `certificate` and `private_key` are replaced together; a replaced certificate takes the request's `chain`, or none. `meta` replaces the stored metadata when given.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `domain` | string | Certificate domain; may be a wildcard such as `*.example.com`. |

### Request body

A [CertificateUpdate](/docs/api/schemas#certificateupdate) object.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `domain` | string | No | Optional; must equal the `domain` in the URL. |
| `certificate` | string | No | PEM-encoded certificate; requires `private_key`. |
| `private_key` | string | No | PEM-encoded private key; requires `certificate`. |
| `chain` | string | No | Used only with a new `certificate`; omitting it clears the chain. |
| `meta` | [CertificateMeta](/docs/api/schemas#certificatemeta) | No | Free-form metadata. Ceryx adds `issued_at` (when not given) and `stored_at` on every write. |

`certificate` requires `private_key`.

`private_key` requires `certificate`.

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The updated certificate (without the private key). | [Certificate](/docs/api/schemas#certificate) |
| `400` | Missing or invalid input. | [Error](/docs/api/schemas#error) |
| `404` | No such resource. | [Error](/docs/api/schemas#error) |
| `500` | The storage backend failed, or a Ceryx environment variable the request needs has an unusable value (`detail` names it). | [Error](/docs/api/schemas#error) |

### Example

```bash
curl -X PUT 'http://ceryx:8080/api/certificates/*.example.com' \
  -H 'Content-Type: application/json' \
  -d '{
    "meta": {
      "vendor": "Example CA",
      "san": [
        "*.example.com"
      ],
      "expires_at": "2027-02-01T00:00:00Z",
      "note": "Renewed by the platform team"
    }
  }'
```

Responds with `200`:

```json
{
  "domain": "*.example.com",
  "has_certificate": true,
  "has_chain": true,
  "meta": {
    "vendor": "Example CA",
    "san": [
      "*.example.com"
    ],
    "expires_at": "2027-02-01T00:00:00Z",
    "note": "Renewed by the platform team",
    "issued_at": "2026-11-10T09:30:00Z",
    "stored_at": 1794303000
  }
}
```

## Delete a stored certificate

`DELETE /api/certificates/{domain}`

Idempotent; deleting a missing certificate also returns 204.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `domain` | string | Certificate domain; may be a wildcard such as `*.example.com`. |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `204` | Deleted. | None |
| `500` | The storage backend failed, or a Ceryx environment variable the request needs has an unusable value (`detail` names it). | [Error](/docs/api/schemas#error) |

### Example

```bash
curl -X DELETE 'http://ceryx:8080/api/certificates/*.example.com'
```

Responds with `204` and no body.
