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:
- Web UI at
/for humans. - MCP endpoint at
POST /mcp(JSON-RPC 2.0) for AI agents that speak MCP. - JSON HTTP API at
POST /api/<tool_name>for everything else (curl, general HTTP clients, agents that don’t speak MCP).
/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:
/orders(web form) - 5 burst, 1 refill per minute./mcpand/api/*- 30 burst, 1 refill per second.
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):
- SANs in the CSR must be exactly
{domain, "www." + domain}(case- and order-insensitive). No additional DNS names. No email, URI, or IP SANs. - Key must be RSA with at least 2048 bits, or ECDSA on P-256 or P-384. DSA, ECDSA P-224, and Ed25519 are rejected.
- CSR signature must verify.
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:
Content-Type: application/jsonX-Beacon-Event: <state>X-Beacon-Order: <order_id>X-Beacon-Signature: sha256=<hex HMAC of the request body using webhook_secret>
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.