---
title: ACME (Let's Encrypt)
path: api-guides/acme
status: published
---

# ACME (Let's Encrypt)

Issue and auto-renew public TLS certificates via any RFC 8555 ACME provider: Let's Encrypt, ZeroSSL, BuyPass, Google Trust Services, or an enterprise ACME server. ScaiVault is the ACME client — you configure it once and it handles challenges, ordering, and renewal.

**Base path:** `/v1/pki/acme/`

## Pick a provider

| Provider | Notes |
|----------|-------|
| `letsencrypt` | Free, widely supported. Rate limits apply — see [https://letsencrypt.org/docs/rate-limits/](https://letsencrypt.org/docs/rate-limits/). |
| `zerossl` | Free tier, 90-day certs. |
| `buypass` | Free 180-day certs. |
| `google` | Google Trust Services. Requires GCP account setup. |
| `custom` | Any RFC 8555 endpoint. Provide `directory_url`. |

Each provider has a `production` and `staging` environment. Use `staging` during development — its rate limits are looser and its certs aren't trusted, so you won't burn through your production quota.

## Register an ACME account

```bash
curl -X POST https://scaivault.scailabs.ai/v1/pki/acme/accounts \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "letsencrypt-production",
    "provider": "letsencrypt",
    "environment": "production",
    "email": "certs@acme.example"
  }'
```

```python
resp = httpx.post(
    "https://scaivault.scailabs.ai/v1/pki/acme/accounts",
    headers={"Authorization": f"Bearer {os.environ['SCAIVAULT_TOKEN']}"},
    json={
        "name": "letsencrypt-production",
        "provider": "letsencrypt",
        "environment": "production",
        "email": "certs@acme.example",
    },
)
```

```typescript
const resp = await fetch("https://scaivault.scailabs.ai/v1/pki/acme/accounts", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SCAIVAULT_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "letsencrypt-production",
    provider: "letsencrypt",
    environment: "production",
    email: "certs@acme.example",
  }),
});
```

Response:

```json
{
  "id": "acme_abc",
  "provider": "letsencrypt",
  "environment": "production",
  "email": "certs@acme.example",
  "status": "active",
  "created_at": "2026-04-23T14:00:00Z"
}
```

ScaiVault generates an account key during registration and stores it internally; you don't see or handle it.

## How it works

```mermaid
sequenceDiagram
    participant App
    participant SV as ScaiVault
    participant ACME as Let's Encrypt
    participant DNS as DNS Provider

    App->>SV: POST /pki/acme/issue<br/>(domains, challenge_type, auto_renew)
    SV->>ACME: create order
    ACME-->>SV: challenges (e.g. TXT _acme-challenge.<br/>example = token)
    SV->>DNS: create TXT record
    DNS-->>SV: ok
    Note over SV: poll authoritative<br/>nameservers
    SV->>ACME: challenge ready
    ACME->>ACME: verify TXT
    ACME-->>SV: order valid + cert
    SV->>DNS: delete TXT record
    SV-->>App: certificate.issued event
```

## Pick a challenge type

Three options, for three situations.

| Challenge | When to use | What ScaiVault needs |
|-----------|-------------|----------------------|
| **`http-01`** | The domain points at a host you control, reachable on port 80. | ScaiVault serves `/.well-known/acme-challenge/<token>` — route that path to it. |
| **`dns-01`** | Wildcards (`*.acme.example`), or the host isn't web-reachable on port 80. | A DNS provider configured in ScaiVault (Route 53, Cloudflare, Google DNS, and others). |
| **`tls-alpn-01`** | Port 80 isn't available. You control port 443. | ScaiVault answers ACME on the TLS-ALPN-01 protocol on port 443. |

DNS-01 is the most reliable — it doesn't care about network topology and supports wildcards. Configure a DNS provider once (see the admin UI under **PKI → DNS Providers**) and use `dns-01` for everything.

## Issue a certificate

```bash
curl -X POST https://scaivault.scailabs.ai/v1/pki/acme/issue \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "acme_abc",
    "domains": ["api.acme.example", "www.acme.example"],
    "challenge_type": "dns-01",
    "auto_renew": true
  }'
```

```python
resp = httpx.post(
    "https://scaivault.scailabs.ai/v1/pki/acme/issue",
    headers={"Authorization": f"Bearer {TOKEN}"},
    json={
        "account_id": "acme_abc",
        "domains": ["api.acme.example", "www.acme.example"],
        "challenge_type": "dns-01",
        "auto_renew": True,
    },
)
```

```typescript
const resp = await fetch("https://scaivault.scailabs.ai/v1/pki/acme/issue", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    account_id: "acme_abc",
    domains: ["api.acme.example", "www.acme.example"],
    challenge_type: "dns-01",
    auto_renew: true,
  }),
});
```

Response while the order is in progress:

```json
{
  "order_id": "acme_order_xyz",
  "status": "pending",
  "domains": ["api.acme.example", "www.acme.example"],
  "challenges": [
    {
      "domain": "api.acme.example",
      "type": "dns-01",
      "status": "pending",
      "token": "...",
      "key_authorization": "..."
    }
  ],
  "created_at": "2026-04-23T14:00:00Z"
}
```

Poll order status:

```bash
curl -H "Authorization: Bearer $TOKEN" \
     https://scaivault.scailabs.ai/v1/pki/acme/orders/acme_order_xyz
```

When complete:

```json
{
  "order_id": "acme_order_xyz",
  "status": "valid",
  "certificate_id": "cert_abc",
  "domains": ["api.acme.example", "www.acme.example"],
  "not_after": "2026-07-22T14:00:00Z"
}
```

Fetch the certificate PEM the same way as any issued cert:

```bash
curl -H "Authorization: Bearer $TOKEN" \
     https://scaivault.scailabs.ai/v1/pki/certificates/cert_abc/pem
```

## Wildcard certificates

Require `dns-01`. Most providers don't accept HTTP-01 for wildcards.

```bash
curl -X POST https://scaivault.scailabs.ai/v1/pki/acme/issue \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "acme_abc",
    "domains": ["*.api.acme.example"],
    "challenge_type": "dns-01",
    "auto_renew": true
  }'
```

## DNS providers

ScaiVault writes the DNS TXT challenge record via an integrated DNS provider. Configure the provider once:

```bash
curl -X POST https://scaivault.scailabs.ai/v1/pki/dns-providers \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "route53-acme",
    "type": "aws_route53",
    "config": {
      "access_key_id_path": "infra/aws/route53/access-key-id",
      "secret_access_key_path": "infra/aws/route53/secret-access-key",
      "hosted_zone_id": "Z1234567890ABC"
    }
  }'
```

Credentials are themselves references to ScaiVault secrets — they don't go in the DNS provider body directly.

Supported providers include AWS Route 53, Cloudflare, Google Cloud DNS, Azure DNS, DigitalOcean, Hetzner, and RFC 2136 dynamic DNS. Provider-specific config keys vary; check the admin UI or [DNS Providers Reference](../reference/pki#dns-providers).

## Auto-renewal

When `auto_renew: true` (default), ScaiVault re-runs the ACME flow ~30 days before `not_after`. Behaviors:

- Re-uses the same account.
- Same domains, same challenge type.
- New certificate becomes a new version of the same "managed certificate" object.
- `certificate.renewed` event fires on success.
- `certificate.renewal_failed` fires on failure, with retries on exponential backoff.

To renew early (before the 30-day window), force it:

```bash
curl -X POST https://scaivault.scailabs.ai/v1/pki/certificates/cert_abc/renew \
  -H "Authorization: Bearer $TOKEN"
```

## List ACME accounts and orders

```bash
curl -H "Authorization: Bearer $TOKEN" https://scaivault.scailabs.ai/v1/pki/acme/accounts
curl -H "Authorization: Bearer $TOKEN" https://scaivault.scailabs.ai/v1/pki/acme/orders
```

Filter orders by status: `pending`, `valid`, `invalid`, `deactivated`, `revoked`, `expired`.

## Revoke

ACME-issued certs can be revoked via ACME (preferred) or the standard revocation endpoint:

```bash
curl -X POST https://scaivault.scailabs.ai/v1/pki/revoke \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"serial_number": "1A:2B:...", "reason": "key_compromise", "use_acme": true}'
```

`use_acme: true` routes the revocation through the ACME server. Without it, ScaiVault only marks the cert revoked in its own store.

## Rate limit awareness

Let's Encrypt rate limits (subject to change):

- 50 certificates per registered domain per week
- 5 duplicate certificates per week
- 300 pending authorizations per account

If you hit them you get `backend_rate_limited` from ScaiVault. Use staging for development and CI.

## What's next

- [PKI Certificates](./pki-certificates) — internal CA and CSR workflows.
- [Events and Webhooks](../core-concepts/events-and-webhooks) — listen for `certificate.renewed`, `certificate.expiring`.
- [PKI Reference](../reference/pki) — all endpoints.
