Beacon API documentation

Beacon issues free 90-day SSL/TLS certificates from Let’s Encrypt. There is no account system, no signup, no wildcards, no renewal automation, and your private key is never stored on the server.

The same flow is exposed three ways:

/mcp and /api/* are driven by the same tool registry, so they cannot drift. Whatever an MCP client can do, a plain HTTP caller can also do.

Authentication

If the server is started with MCP_TOKEN set, every request to /mcp and /api/* must carry the bearer token:

Authorization: Bearer <token>

X-Beacon-Token: <token> is also accepted. Token comparison is constant-time. When MCP_TOKEN is empty, both surfaces are open (the dev default).

Rate limits

Two buckets, both keyed by client IP:

Both protect the Let’s Encrypt account’s 300-orders-per-3-hours quota. Exceeding either bucket returns 429 Too Many Requests with Retry-After: 30.

Discovery

Path Purpose
GET /openapi.json OpenAPI 3.1 spec for every /api/* endpoint.
GET /.well-known/mcp.json Custom MCP discovery doc - tools, endpoint URL, auth scheme.
GET /llms.txt Agent-oriented quickstart in plain markdown.
GET /docs This page.

The .well-known/mcp.json document is not part of a published MCP standard. The Model Context Protocol negotiates capabilities inside the JSON-RPC initialize call against /mcp; that is the authoritative discovery mechanism. The well-known file is purely a hint - its discovery field points back at /mcp so callers know where to do the real handshake. The shape may evolve as MCP-client conventions converge; consumers should not treat its fields as a contract.

Tools

Every tool below is available at both POST /mcp (as a JSON-RPC tools/call) and POST /api/<tool_name> (as a plain JSON POST). Inputs and outputs are identical.

create_order

Create a new certificate order.

Input:

{
  "domain": "example.com",
  "email": "[email protected]",
  "marketing_consent": false,
  "webhook_url": "https://your-app.example/hook"
}

marketing_consent and webhook_url are optional. Passing webhook_url returns a one-time webhook_secret used to sign callback bodies with HMAC-SHA256.

Output:

{
  "order_id": "...",
  "state": "dns_pending",
  "domain": "example.com",
  "dns_records": [
    {"type": "TXT", "name": "_acme-challenge.example.com", "value": "..."},
    {"type": "TXT", "name": "_acme-challenge.www.example.com", "value": "..."}
  ],
  "next_action": "publish the TXT records above, then call check_propagation",
  "webhook_secret": "..."
}

The certificate always covers domain and www.domain - no other SANs, no wildcards. The input domain is normalized: lowercased, leading www. stripped, and IDN-converted to ASCII before storage.

check_propagation

Query Cloudflare (1.1.1.1), Google (8.8.8.8), and Quad9 (9.9.9.9) in parallel for each TXT record. Each resolver result is reported individually; the top-level all_found is true only when every record is visible on every resolver.

Input: { "order_id": "..." }

Output:

{
  "order_id": "...",
  "all_found": true,
  "records": [
    {
      "name": "_acme-challenge.example.com",
      "all_found": true,
      "resolvers": [
        {"resolver": "Cloudflare", "found": true},
        {"resolver": "Google",     "found": true},
        {"resolver": "Quad9",      "found": true}
      ]
    }
  ]
}

Call this before validate_order. Failed Let’s Encrypt validations count against a 5-per-hour-per-hostname quota; pre-flight DNS checks avoid burning that quota on DNS that hasn’t propagated yet.

validate_order

Submit the DNS challenges to Let’s Encrypt. The order transitions to validating. Poll get_order_status (or wait for a webhook) to discover whether the authorizations passed.

Input: { "order_id": "..." }

Output: { "order_id": "...", "state": "validating" }

get_order_status

Non-blocking status poll. Returns the current order state plus per-authz statuses from Let’s Encrypt.

Input: { "order_id": "..." }

Output:

{
  "order_id": "...",
  "state": "ready",
  "domain": "example.com",
  "authz": {
    "https://acme/authz/abc": "valid",
    "https://acme/authz/def": "valid"
  }
}

Possible states: dns_pending, validating, ready, completed, failed, expired.

issue_certificate

Finalize the order. Beacon generates an RSA-2048 keypair in memory, builds a PKCS#12 bundle containing the leaf certificate, the intermediate chain, and that private key, all encrypted with the supplied passphrase, and returns the bundle base64-encoded.

Input:

{
  "order_id": "...",
  "passphrase": "your-strong-passphrase"
}

The passphrase must be at least 8 characters. Beacon does not store it; you will re-enter it when importing the bundle on your server.

Output:

{
  "order_id": "...",
  "state": "completed",
  "filename": "example.com_a1b2c3d4.p12",
  "p12_base64": "MIIK...",
  "content_type": "application/x-pkcs12",
  "passphrase_hint": "import the bundle with the passphrase you supplied",
  "renewal_reminder": "Let's Encrypt certificates expire after 90 days",
  "renewal_tracker": "https://tlsradar.com"
}

The filename has the form <domain>_<serial>.p12, where serial is the last 8 hex characters of the issued certificate’s serial number. The same filename is used for the SMTP attachment (when email delivery is configured) so the downloaded and emailed copies match.

issue_with_csr

An alternative to issue_certificate for callers who manage their own private keys. Supply a PEM-encoded CSR; Beacon validates it, finalizes the order, and returns the issued certificate chain as PEM. The private key never enters Beacon.

Input:

{
  "order_id": "...",
  "csr_pem": "-----BEGIN CERTIFICATE REQUEST-----\n..."
}

Validation rules (rejection returns 400 + an error message):

Output:

{
  "order_id": "...",
  "state": "completed",
  "filename": "example.com_a1b2c3d4.crt",
  "leaf_pem": "-----BEGIN CERTIFICATE-----\n...",
  "chain_pem": "-----BEGIN CERTIFICATE-----\n...",
  "fullchain_pem": "-----BEGIN CERTIFICATE-----\n...",
  "not_after": "2026-08-23T14:30:00Z",
  "renewal_reminder": "Let's Encrypt certificates expire after 90 days",
  "renewal_tracker": "https://tlsradar.com"
}

leaf_pem is the issued certificate alone. chain_pem is the intermediate chain only. fullchain_pem is the leaf followed by the intermediates - the form most servers (nginx, Caddy, HAProxy) want.

Because the agent owns the private key, Beacon does not deliver an email copy and does not create a signed download link. The certificate material is returned only in this response.

finalize_order

A composite that collapses the three most error-prone steps - validate_order, polling get_order_status until ready, and the issue call - into one server-side operation. Call it after check_propagation reports all_found: true. Supply exactly one of passphrase (PKCS#12 path, mirrors issue_certificate) or csr_pem (bring-your-own-key path, mirrors issue_with_csr).

Beacon submits the challenges, then polls Let’s Encrypt server-side for up to max_wait_seconds (default 60, capped at 75 to stay within the request timeout). If validation completes, it issues and returns the bundle; if it doesn’t finish in time, it returns an error asking you to keep polling get_order_status and then call issue_certificate.

Input:

{
  "order_id": "...",
  "passphrase": "at-least-8-chars",
  "csr_pem": "(omit when using passphrase)",
  "email_copy": false,
  "max_wait_seconds": 60
}

Output: mode is "pkcs12" or "csr" and selects which fields are populated - the PKCS#12 fields (p12_base64, filename, …) or the PEM fields (leaf_pem, chain_pem, fullchain_pem, not_after).

renew_order

Clone a completed (or ready, failed, or expired) order into a fresh issuance using the same domain and email. Returns a new order ID; the original is linked through parent_order_id. Rate-limited together with create_order because it opens a new ACME order.

Input: { "order_id": "..." }

Output:

{
  "order_id": "...",
  "parent_order_id": "...",
  "state": "dns_pending",
  "domain": "example.com",
  "dns_records": [...],
  "next_action": "publish the TXT records above, then call check_propagation"
}

list_order_events

Return the append-only event log for an order. Includes state transitions plus auxiliary events like webhook_sent, webhook_failed, mailed, and download_delivered.

Input: { "order_id": "..." }

Output:

{
  "order_id": "...",
  "events": [
    {"kind": "created",    "state": "dns_pending", "created_at": 1700000000},
    {"kind": "validating", "state": "validating",  "created_at": 1700000060},
    {"kind": "ready",      "state": "ready",       "created_at": 1700000120},
    {"kind": "completed",  "state": "completed",   "created_at": 1700000150}
  ]
}

Webhooks

Pass an HTTPS webhook_url on create_order and Beacon returns a 32-byte hex webhook_secret (returned once - persist it). Every persisted state transition POSTs to that URL with:

Delivery is fire-and-forget with two attempts (t+0 and t+3s) and then logged. The event log is authoritative; webhooks are a hint to poll get_order_status.

Order state machine

            dns_pending
                 |
                 v
            validating
              /    \
             v      v
        failed     ready
                     |
                     v
                completed

Any non-terminal order older than 24 hours is reported as expired. Beacon purges expired rows on startup; there is no background loop.

Example: full agent loop with curl

# 1. Create the order.
curl -sS -X POST https://beacon.tlsradar.com/api/create_order \
  -H 'Content-Type: application/json' \
  -d '{"domain":"example.com","email":"[email protected]"}'

# 2. Publish the returned TXT records in your DNS. Then verify propagation.
curl -sS -X POST https://beacon.tlsradar.com/api/check_propagation \
  -H 'Content-Type: application/json' \
  -d '{"order_id":"<id>"}'

# 3. Once all_found is true, submit to Let's Encrypt.
curl -sS -X POST https://beacon.tlsradar.com/api/validate_order \
  -H 'Content-Type: application/json' \
  -d '{"order_id":"<id>"}'

# 4. Poll until state == "ready".
curl -sS -X POST https://beacon.tlsradar.com/api/get_order_status \
  -H 'Content-Type: application/json' \
  -d '{"order_id":"<id>"}'

# 5. Issue. The response is a base64 PKCS#12 bundle.
curl -sS -X POST https://beacon.tlsradar.com/api/issue_certificate \
  -H 'Content-Type: application/json' \
  -d '{"order_id":"<id>","passphrase":"correct horse battery staple"}'

Add Authorization: Bearer <token> to every call when the server is configured with MCP_TOKEN.