# Domainee — full documentation > Domainee is a custom domains API for SaaS with a native MCP server — 50 domains and 100 GB free. This file inlines every Domainee guide and API reference page so an agent can answer questions and write working requests from a single fetch. The linked map version is at https://domainee.dev/llms.txt. Generated from the same source as https://domainee.dev/docs. ## Product summary - **Connect API** (`POST /v1/domains`): a user brings their own domain, Domainee returns a CNAME target and handles SSL issuance and renewal (Let's Encrypt/ACME) forever, with DNS health monitoring and webhooks. - **Buy a Domain API** (`POST /v1/domain-purchases`): register domains in-app across 500+ TLDs through the customer's own Stripe account, at wholesale price plus a $1 flat fee; the end user is the legal registrant. - **MCP server**: 11 workspace tools (with an API key) so AI agents (Claude, Cursor, and others) can connect, verify, and manage domains directly, plus 16 free diagnostic tools (SSL/DNS/WHOIS lookups) usable with no key. Domainee is the only custom domains API that is MCP-native. - **16 free public REST endpoints** under `https://api.domainee.dev/v1/tools/` with no key and no signup, documented in full below. Pricing: 50 custom domains and 100 GB bandwidth/month free forever, no credit card. Then $0.20 per domain/month with graduated discounts down to $0.10, 400 GB bandwidth included, $0.05/GB after. Fully self-serve. ## Install the MCP server Hosted, stateless MCP endpoint: `https://mcp.domainee.dev/mcp`. The 16 free diagnostic tools work with no auth; the 11 workspace tools need a Bearer Domainee API key from /developers. - Claude Code: `claude mcp add --transport http domainee https://mcp.domainee.dev/mcp --header "Authorization: Bearer sk_live_YOUR_KEY"` - JSON config (Claude Desktop, Cursor, Windsurf, Continue, Cline): `{"mcpServers":{"domainee":{"url":"https://mcp.domainee.dev/mcp","headers":{"Authorization":"Bearer sk_live_YOUR_KEY"}}}}` - Workspace tools (11, require a Bearer API key): list_domains, get_domain, create_domain, update_domain, delete_domain, check_domain, list_webhook_endpoints, create_webhook_endpoint, delete_webhook_endpoint, dns_check_records_exist, dns_check_records_match_exactly - Free diagnostic tools (16, no key — an unauthenticated tools/list returns these): tools_ssl_check, tools_dns_record_lookup, tools_whois_lookup, tools_cname_lookup, tools_http_header_checker, tools_dns_propagation_checker, tools_redirect_checker, tools_spf_record_checker, tools_dkim_record_checker, tools_dmarc_record_checker, tools_txt_record_lookup, tools_domain_age_checker, tools_domain_availability_checker, tools_subdomain_finder, tools_reverse_ip_lookup, tools_website_status_checker - Source & registry: GitHub https://github.com/CommonNinja/domainee-mcp-server · official MCP registry `dev.domainee/domainee` · Glama https://glama.ai/mcp/connectors/dev.domainee/domainee --- ## Introduction Source: https://domainee.dev/docs Connect your customers' domains to your origins, with automatic SSL, in two API calls. Welcome to Domainee. This product lets your customers point their own domains at your application — fully automated TLS, no DNS-validation back-and-forth, single CNAME for the customer. ### What you get - **Zero-touch SSL.** When a request first arrives for a customer's domain, we issue a cert and serve traffic. Renewal is automatic forever. - **A single REST API.** Create domains, route them to your origins, listen to webhooks. That's the whole API surface. - **Predictable pricing.** First 50 domains free, then $0.20/domain with volume discounts to 50% off. Bandwidth is 400 GB included + $0.05/GB. See [Pricing](https://domainee.dev/docs/pricing). ### How it works ``` Your customer's user → shop.acme.com │ CNAME to edge.domainee.dev │ ▼ Domainee TLS edge (issues cert, terminates TLS) │ ▼ Domainee L7 edge (looks up origin from your config) │ ▼ Your origin (e.g. https://acme.fly.dev) ``` The customer publishes a single CNAME. The first time someone hits it, we provision a Let's Encrypt cert (with on-demand TLS). After that, traffic flows through us to your origin URL. ### What you do 1. **Sign up** at [domainee.dev/sign-up](https://domainee.dev/sign-up). No credit card required for the free tier (50 domains, 100 GB bandwidth/month). 2. **Mint an API key** from the Developers page in your dashboard. 3. **Call `POST /v1/domains`** for each customer hostname you want to route. 4. **Tell your customer** to publish one CNAME. We auto-detect when they do and flip the domain status to `verified` within 60 seconds. The [Quickstart](https://domainee.dev/docs/quickstart) walks through the whole flow with copy-pastable commands. ### Where to go next - **[Quickstart](https://domainee.dev/docs/quickstart)** — your first domain in 5 minutes - **[Authentication](https://domainee.dev/docs/authentication)** — API keys and Bearer tokens - **[Domains API](https://domainee.dev/docs/api/domains/createDomain)** — full reference - **[Webhooks](https://domainee.dev/docs/webhooks)** — get notified when domain status changes - **[MCP server](https://domainee.dev/docs/mcp)** — manage Domainee from Claude Desktop, Cursor, or any MCP client - **[Errors](https://domainee.dev/docs/errors)** — error codes, retries, idempotency ## Quickstart Source: https://domainee.dev/docs/quickstart Connect your first custom domain end-to-end in five minutes. By the end of this guide you'll have one of your customers' domains routing through Domainee to your origin, with automatic HTTPS. ### 1. Sign up and get an API key 1. [Create an account](https://domainee.dev/sign-up). 2. Open the [Developers](https://domainee.dev/developers) page in your dashboard. 3. Click **New key**, name it (e.g. `production`), copy the key — it's shown only once. Format: `sk_live_…`. ```bash export DOMAINEE_API_KEY=sk_live_... ``` ### 2. Register a domain Replace `shop.acme.com` with the customer hostname you want to route, and `https://acme.fly.dev` with your origin URL (the actual app the traffic should reach). ```bash curl -X POST https://api.domainee.dev/v1/domains \ -H "Authorization: Bearer $DOMAINEE_API_KEY" \ -H "content-type: application/json" \ -d '{ "hostname": "shop.acme.com", "originUrl": "https://acme.fly.dev" }' ``` You'll get back the domain object plus a list of DNS records the customer needs to publish: ```json { "domain": { "id": "8f09b47c-b42f-4d14-8395-2989db76e6f8", "hostname": "shop.acme.com", "originUrl": "https://acme.fly.dev", "status": "pending", "mode": "proxy", "dnsRecords": [ { "type": "CNAME", "name": "shop.acme.com", "value": "edge.domainee.dev", "purpose": "Traffic Routing" } ] }, "warnings": [] } ``` See [Create a domain](https://domainee.dev/docs/api/domains/createDomain) for every parameter. ### 3. Tell your customer what to add at their DNS provider Hand them the single CNAME from the response: ``` Type: CNAME Name: shop.acme.com Value: edge.domainee.dev ``` That's it — no validation record, no second CNAME for SSL. (If they're on Cloudflare, set the proxy status to **DNS only** so TLS passes through.) ### 4. Wait for verification Within 60 seconds of the customer publishing the CNAME, our DNS monitor detects it and flips `status` from `pending` to `verified`. You can also force an immediate check: ```bash DOMAIN_ID=8f09b47c-b42f-4d14-8395-2989db76e6f8 curl -X POST https://api.domainee.dev/v1/domains/$DOMAIN_ID/check \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` ### 5. First HTTPS request ```bash curl -i https://shop.acme.com/some/path ``` The first request takes 10–30 seconds: our TLS edge sees a new SNI, asks our control plane "is this a verified domain?", then provisions a Let's Encrypt cert. After that, every request uses the cached cert (no LE round-trip). ### 6. Listen for status changes Subscribe to webhooks so you know when a domain is verified, when DNS breaks, when SSL expires, etc.: ```bash curl -X POST https://api.domainee.dev/v1/webhook-endpoints \ -H "Authorization: Bearer $DOMAINEE_API_KEY" \ -H "content-type: application/json" \ -d '{ "url": "https://your-app.com/webhooks/domainee", "events": [] }' ``` (Empty `events` array subscribes to all domain events.) See [Webhooks](https://domainee.dev/docs/webhooks) for signature verification and the full event list. ### What's next - [Domains API reference](https://domainee.dev/docs/api/domains/createDomain) — every field, every endpoint - [Webhook events](https://domainee.dev/docs/api/events) — what fires and when - [Errors](https://domainee.dev/docs/errors) — `monitor_status` values, retries, idempotency ## Authentication Source: https://domainee.dev/docs/authentication Bearer-token auth with API keys you mint from the dashboard. The Domainee REST API uses **Bearer-token authentication** with API keys you mint from the [Developers](https://domainee.dev/developers) page. ### API key format ``` sk_live_<48 random base64url chars> ``` Total length: 56 characters. The `sk_live_` prefix is constant; the rest is cryptographically random. ### Sending the key Every request to `https://api.domainee.dev/v1/*` must include: ``` Authorization: Bearer sk_live_... ``` Example: ```bash curl https://api.domainee.dev/v1/domains \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` The header name and `Bearer` scheme are case-insensitive (`authorization: bearer` works too), per RFC 7235. ### Workspace scoping Every API key belongs to **exactly one workspace** — the workspace it was created in. All resources you create with that key (domains, webhook endpoints) belong to the same workspace and are billed against its subscription. If you have multiple workspaces, each one needs its own key. ### Managing keys In the dashboard at [/developers](https://domainee.dev/developers): - **New key** — generates a fresh key. Plaintext is shown ONCE in a modal — copy it before closing. - **Revoke** — invalidates the key immediately. Any in-flight requests with the revoked key get 401. We never store the plaintext server-side; only a SHA-256 hash and the first 16 characters (the `sk_live_xxxx…` prefix) for display in the dashboard. ### Storing keys safely - **Treat keys as production credentials.** Keep them in your secret manager (Doppler, AWS Secrets Manager, Railway env vars, etc.). - **Don't commit them** to git or include them in client-side code. They're server-side only — anyone with a key can manage all domains in your workspace. - **Rotate on compromise.** Revoke + mint a new key whenever you suspect leakage. - **Use one key per environment.** Production app uses one key, staging uses another. If staging leaks, only staging is exposed. ### Test credentials Domainee currently runs in live mode only. Use a workspace dedicated to testing if you need a sandbox. ### Errors | Status | When it fires | |---|---| | `401 unauthorized` | Missing `Authorization` header | | `401 unauthorized` | API key not found, malformed, or revoked | | `429 rate_limited` | Exceeded 60 req/min per key (configurable on request) | A revoked key returns `401` with `{"error":"unauthorized","message":"Invalid API key"}`. ## Webhooks Source: https://domainee.dev/docs/webhooks HMAC-signed webhook deliveries with automatic retries. Webhooks let you react to domain status changes without polling. We POST a JSON payload to your endpoint when a domain is created, verified, fails, or its monitor state changes. For the endpoint-management API see [Webhook endpoints](https://domainee.dev/docs/api/webhooks/createEndpoint). For each event's payload shape see [Webhook events](https://domainee.dev/docs/api/events). ### Delivery format Every webhook delivery includes: ``` x-domainee-signature: sha256= x-domainee-event: domain.verified x-domainee-delivery-id: content-type: application/json ``` The body is the JSON envelope: ```json { "id": "", "type": "domain.verified", "createdAt": "2026-05-05T11:39:19.406Z", "data": { ... } } ``` ### Verifying signatures The signature is `HMAC-SHA256(secret, raw_request_body)`. You **must** verify it before trusting the payload — otherwise anyone can forge events to your endpoint. #### Node.js example ```js function verifyDomaineeWebhook(rawBody, headerSig, secret) { const expected = "sha256=" + crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex"); return crypto.timingSafeEqual( Buffer.from(headerSig), Buffer.from(expected), ); } app.post("/webhooks/domainee", express.raw({ type: "application/json" }), (req, res) => { const sig = req.header("x-domainee-signature"); if (!sig || !verifyDomaineeWebhook(req.body, sig, process.env.DOMAINEE_WEBHOOK_SECRET)) { return res.status(401).end(); } const event = JSON.parse(req.body.toString()); res.status(200).end(); }); ``` `req.body` must be the **raw bytes**, not a parsed JSON object. Frameworks that auto-parse JSON will produce different bytes when re-serialized, breaking verification. #### Why a custom header (vs. the more common `Stripe-Signature`)? Custom-named so host-routing proxies in front of your webhook receiver (Railway, Render, ALB host-based rules, etc.) don't rewrite or strip it. ### Retry behavior A delivery is considered successful when your endpoint returns a 2xx. Anything else (timeout, 4xx, 5xx) triggers retries on this schedule from the moment of the original event: | Attempt | Time after event | |---|---| | 1 | immediate | | 2 | +1 minute | | 3 | +5 minutes | | 4 | +30 minutes | | 5 | +2 hours | | 6 | +12 hours | After attempt 6, delivery is given up and marked as failed. ### Idempotency The `x-domainee-delivery-id` header is unique per delivery attempt. The payload's top-level `id` field is the **event id** — the same across retries of the same event. Dedupe on the event id. ### Filtering on your side The `x-domainee-event` header tells you the type before you parse the body — useful for routing to different handlers. ```js const eventType = req.header("x-domainee-event"); switch (eventType) { case "domain.verified": return handleVerified(req.body); case "domain.monitor_updated": return handleMonitor(req.body); default: return res.status(200).end(); } ``` ## MCP server (use Domainee from AI tools) Source: https://domainee.dev/docs/mcp Connect Claude Desktop, Cursor, or any MCP client to manage your domains in natural language. Domainee ships a [Model Context Protocol](https://modelcontextprotocol.io) server at `https://mcp.domainee.dev/mcp`. Once you wire it into your AI client, prompts like _"list my failing domains and tell me which DNS records are wrong"_ or _"redirect shop.acme.com to https://acme-store.fly.dev"_ just work — the model calls the same REST API as your code, scoped to the same workspace as your API key. ### Setup You need: 1. An API key from the [Developers](https://domainee.dev/developers) page (`sk_live_…`). 2. An MCP-aware client: Claude Desktop, Cursor, Cline, etc. #### Claude Desktop / Cursor Add to your `claude_desktop_config.json` (Claude Desktop) or `mcp.json` (Cursor): ```json { "mcpServers": { "domainee": { "url": "https://mcp.domainee.dev/mcp", "headers": { "Authorization": "Bearer sk_live_..." } } } } ``` Restart the app. Domainee's tools should appear in the MCP tools list, ready to call. #### From a script (raw JSON-RPC) Each tool call is a single HTTP POST. Useful for automation: ```bash curl https://mcp.domainee.dev/mcp \ -H "Authorization: Bearer $DOMAINEE_API_KEY" \ -H "content-type: application/json" \ -H "accept: application/json, text/event-stream" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_domains", "arguments": { "status": "verified" } } }' ``` ### What the model can do Eleven tools across three resource groups, all backed by the same REST API documented under [API Reference](https://domainee.dev/docs/api). #### Domains | Tool | What it does | |---|---| | `list_domains` | Paginated list with optional status filter | | `get_domain` | Fetch a single domain by id | | `create_domain` | Register a new customer hostname | | `update_domain` | Change `originUrl` / `mode` / redirect flags | | `delete_domain` | Stop routing a hostname | | `check_domain` | Force an immediate DNS / SSL probe | #### Webhook endpoints | Tool | What it does | |---|---| | `list_webhook_endpoints` | List endpoints (secrets stripped) | | `create_webhook_endpoint` | Register a new HTTPS endpoint, returns the signing secret once | | `delete_webhook_endpoint` | Stop sending events to an endpoint | #### DNS checks | Tool | What it does | |---|---| | `dns_check_records_exist` | "At least one DNS value matches" — for verifying customer CNAMEs | | `dns_check_records_match_exactly` | Stricter: every returned value must match (no extras) | ### Things to try - "List my domains and tell me which ones have monitor status `dns_incorrect`." - "Create a domain `shop.acme.com` proxying to `https://acme-store.fly.dev` and turn on the www variant." - "For each pending domain, check its DNS and tell me whether the customer published the right CNAME." - "Delete every domain whose hostname starts with `test-`." - "Set up a webhook to `https://my-app.com/webhooks/domainee` for verification + failure events." The model will pick the right tool, fill in the arguments, and surface the API response back to you — including any errors (e.g. 402 billing_required if the workspace doesn't have a payment method on file, 409 conflict if the hostname is already taken). ### Auth model Every MCP request includes your `Authorization: Bearer sk_live_…` header. The MCP server forwards that token verbatim to `api.domainee.dev/v1/*`. Auth, rate limits, and the billing gate run on the API server — the MCP layer keeps no state. That means: - Your token = your access. Don't paste it into shared chat threads. - Rate limits (60 req/min per key by default — see [Errors → Rate limits](https://domainee.dev/docs/errors#rate-limits)) apply to MCP traffic the same way they apply to direct API calls. - 402 / 429 / 401 errors surface in the AI client as tool failures, so the model can decide whether to retry, ask you to add a card, etc. ### Limitations - **HTTP transport only.** No stdio mode — the auth model is "pass a Bearer header per request", not "spawn a process per user". - **Tool results are stringified JSON.** Long lists of domains expand the model's context; if you hit context limits, ask it to filter server-side (`status: "failed"`, `limit: 20`, etc.) instead of fetching everything and slicing client-side. - **Read + write parity with the REST API.** Anything the API doesn't expose (analytics, per-domain bandwidth) isn't available via MCP yet either. File a feature request if you need it. ### Source The MCP server source is part of the Domainee monorepo at [`mcp-server/`](https://github.com/CommonNinja/domainee-app/tree/main/mcp-server) — each tool is a small file under `src/tools/`. PRs welcome if you want to add a tool we don't expose yet. ## Errors and edge cases Source: https://domainee.dev/docs/errors HTTP status codes, monitor states, idempotency, and rate limits. Every API error returns a JSON body of the shape: ```json { "error": "", "message": "", "details": { ... } } ``` `details` is sometimes present — for example, a list of preflight warnings when domain creation fails preflight. ### HTTP status reference | Status | When | |---|---| | `200` | Success | | `201` | Resource created | | `400` | `bad_request` — your input is invalid (bad hostname, bad URL) | | `400` | `preflight_failed` — domain create blocked by CAA / SSRF | | `401` | `unauthorized` — missing or invalid Bearer token | | `402` | `billing_required` — free quota exceeded, no payment method on file | | `403` | `forbidden` — token valid, but action not allowed (workspace mismatch) | | `404` | `not_found` — the resource id doesn't exist (or doesn't belong to you) | | `409` | `conflict` — hostname already in use | | `429` | `rate_limited` — too many requests; check `Retry-After` header | | `500` | `internal_error` — our problem; report it | ### Monitor states The `monitorStatus` field on a Domain reflects runtime health, computed from DNS + cert + origin reachability signals every ~60 seconds. The accompanying `monitorMessage` gives a customer-facing explanation. | `monitorStatus` | What it means | What to show your customer | |---|---|---| | `unknown` | We haven't probed yet | "Setup in progress" | | `dns_not_resolving` | The hostname doesn't resolve at all | "Add the CNAME record at your registrar" | | `dns_incorrect` | DNS resolves but not to our edge | "Update your CNAME — it points elsewhere" | | `pending_ssl` | DNS is correct, waiting for cert | "Almost there — SSL is being issued" | | `active_ssl` | Verified and serving traffic | "Your site is live" (✅) | | `target_not_loading` | (Reserved for future origin health checks) | — | | `ssl_failed` | Cert issuance failed irrecoverably | "We couldn't issue an SSL cert. Check CAA records." | | `ssl_expired` | The cert window elapsed without renewal | "Your domain's certificate has expired. Re-verify DNS." | ### Idempotency For idempotent retries on `POST` requests, send an `Idempotency-Key` header. Same key + same path + same workspace within 24 hours returns the cached original response, with header `idempotent-replay: true`. ```bash curl -X POST https://api.domainee.dev/v1/domains \ -H "Authorization: Bearer $DOMAINEE_API_KEY" \ -H "Idempotency-Key: order-123-domain-create" \ -H "content-type: application/json" \ -d '{ "hostname": "shop.acme.com", "originUrl": "https://acme.fly.dev" }' ``` The key is scoped to your workspace — different workspaces with the same key don't collide. ### Rate limits Default: **60 requests per minute per API key** across all `/v1/*` endpoints. Some endpoints have additional, tighter limits: | Endpoint | Per-key limit | |---|---| | `POST /v1/domains` | 100/hour | | `POST /v1/domains/bulk` | 5/hour (×100 domains = 500 hostnames/hour ceiling) | | `POST /v1/domains/:id/check` | 60/min | | `/v1/dns/check-records-*` | 600/hour | When you hit a limit you get: ``` HTTP/1.1 429 Too Many Requests Retry-After: 42 x-ratelimit-limit: 60 x-ratelimit-remaining: 0 x-ratelimit-reset: 1714572420 { "error": "rate_limited", "message": "Too many requests for action 'create_domain'. Try again in 42s.", "details": { "action": "create_domain", "retryAfter": 42 } } ``` Respect `Retry-After` (in seconds) to avoid hammering us during cool-down. ### SSRF protection When you submit an `originUrl`, we resolve its hostname and reject any value that points at: - Private RFC 1918 ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) - Loopback (127.0.0.0/8, ::1) - Link-local — including AWS/GCP metadata (169.254.169.254) - CGNAT (100.64.0.0/10) - Multicast and reserved ranges - Hostnames like `localhost`, `*.local`, `*.internal` This protects us (and you) from being used as a tunnel to internal infrastructure. ### CAA records If a customer's domain has CAA records that don't authorize Let's Encrypt or ZeroSSL, cert issuance will fail when first traffic arrives. We surface this as a **preflight warning** when you create the domain: ```json { "warnings": [ { "code": "caa_blocks_lets_encrypt", "message": "This domain has CAA records that don't authorize Let's Encrypt or ZeroSSL...", "caaRecords": ["digicert.com"] } ] } ``` Treat this as fatal even though the domain is created — traffic will never work until the customer fixes their CAA. The customer needs to add a CAA record permitting `letsencrypt.org` or remove their existing CAA records. ### When in doubt - Check the `x-domainee-request-id` response header on any failure — paste it in support requests so we can find the exact log line. - The dashboard's [Developers](https://domainee.dev/developers) page shows recent API calls per key. ## Pricing Source: https://domainee.dev/docs/pricing How billing works, what counts toward usage, and how the free tier is enforced. For the marketing pricing breakdown with an interactive estimator, see [/pricing](https://domainee.dev/pricing). This page covers the billing mechanics for developers integrating against the API. ### What you pay for Two usage axes, both metered automatically: 1. **Custom domains.** Count of `Domain` rows in your workspace. The first 50 are free; beyond that, $0.20/domain/month with volume discounts that reach 50% off at 10,001+ domains. 2. **Bandwidth.** Total bytes (request + response bodies) flowing through the edge for your domains. 400 GB included with any active subscription; $0.05/GB above. Free-tier accounts (no card on file) get 100 GB/month before traffic is blocked. Both axes appear as separate line items on the same monthly invoice. ### How quantity gets to Stripe - **Domains**: every `POST /v1/domains` and `DELETE /v1/domains/{id}` updates the subscription item's quantity in real time. Stripe prorates mid-cycle changes automatically. A reconciliation worker also re-pushes the count every 30 minutes as a safety net against drift. - **Bandwidth**: the edge counts bytes per request, buffers them in memory, and flushes to our control plane every 30 seconds. We aggregate to daily totals and push usage records to Stripe hourly with backdated timestamps so cycle boundaries are handled correctly. ### When am I charged? End of each monthly billing cycle. Stripe runs the graduated tier formula on your final domain count and sums all bandwidth usage records, generates an invoice, and charges the card on file automatically. If your usage stayed within the free quotas (≤50 domains and ≤400 GB on a paid plan; ≤50 and ≤100 GB on free), the invoice is **$0** and your card isn't touched. ### What happens at the limits | Scenario | Behavior | |---|---| | Free tier, attempting to create domain #51 | `POST /v1/domains` returns `402 billing_required`. Add a card to continue. | | Free tier, exceeded 100 GB this month | Customer traffic returns `402` at the edge until a card is added. New domain creates also blocked with `402`. | | Paid plan, 51st domain | Allowed. End-of-cycle invoice includes the chargeable domain count. | | Paid plan, 401 GB used | Allowed. End-of-cycle invoice includes 1 GB × $0.05 = $0.05 overage. | | Subscription `past_due` (failed payment) | New `POST /v1/domains` returns `402`. Existing traffic continues serving for ~3 weeks of dunning, then traffic is cut off. | | Subscription canceled | Edge returns `404` on every customer hostname for that workspace. | ### Admin bypass Workspaces owned by a platform admin (`users.isAdmin = true`) skip every billing gate — useful for internal/test workspaces. ### Volume discounts Apply automatically based on your **total** domain count (not chargeable count — the 50 free domains count toward your tier). | Domain count | Per-domain | Discount | |---|---|---| | 51–1,000 | $0.20 | — | | 1,001–2,000 | $0.19 | 5% off | | 2,001–3,000 | $0.18 | 10% off | | 3,001–4,000 | $0.17 | 15% off | | 4,001–5,000 | $0.16 | 20% off | | 5,001–6,000 | $0.15 | 25% off | | 6,001–7,000 | $0.14 | 30% off | | 7,001–8,000 | $0.13 | 35% off | | 8,001–9,000 | $0.12 | 40% off | | 9,001–10,000 | $0.11 | 45% off | | 10,001+ | $0.10 | 50% off (cap) | Tiers are **graduated** — domains within each bracket are charged at that bracket's rate. So 1,500 domains = 50 free + 950 × $0.20 + 500 × $0.19 = **$285/mo**. ### Self-serve billing Customers manage their card, view invoices, and cancel from the Stripe-hosted **Customer Portal**, accessible from the dashboard's Billing page. You can drop them straight into the portal via: ```bash POST https://domainee.dev/api/billing/portal ``` (Authenticated via dashboard session — not API key. The Customer Portal isn't an API operation; it's a self-service UI for end users.) ## API Reference ### API overview Source: https://domainee.dev/docs/api Base URL, request format, and the resources exposed by the Domainee REST API. The Domainee REST API runs at: ``` https://api.domainee.dev/v1 ``` All requests are JSON over HTTPS, authenticated with a Bearer token. See [Authentication](https://domainee.dev/docs/authentication) for the key format and how to send it. #### Resources | Resource | What it represents | |---|---| | **[Domains](https://domainee.dev/docs/api/domains/createDomain)** | Customer hostnames routed through our edge to your origins. | | **[Webhook endpoints](https://domainee.dev/docs/api/webhooks/createEndpoint)** | URLs we POST to when domain events fire. | | **[Webhook events](https://domainee.dev/docs/api/events)** | Event types and payload shapes. | | **[DNS checks](https://domainee.dev/docs/api/dns/checkRecordsExist)** | On-demand DNS lookups for verifying customer records. | #### Conventions - All timestamps are ISO 8601 (UTC), e.g. `2026-05-05T11:39:19.406Z`. - All ids are UUIDv4 strings. - `application/json` is the only supported content type for request bodies. - Errors share the shape `{ "error": "", "message": "...", "details": {...} }` — see [Errors](https://domainee.dev/docs/errors) for the full reference. #### Quick example ```bash curl https://api.domainee.dev/v1/domains \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` ```json { "domains": [ { "id": "8f09b47c-b42f-4d14-8395-2989db76e6f8", "hostname": "shop.acme.com", "originUrl": "https://acme.fly.dev", "status": "verified", "monitorStatus": "active_ssl" } ], "nextCursor": null } ``` ### Domains #### Create a domain Source: https://domainee.dev/docs/api/domains/createDomain POST /v1/domains — register a customer hostname. ``` POST /v1/domains ``` Registers a customer hostname for proxying or redirecting through our edge. Counts toward your domain quota immediately. ##### Request ```bash curl -X POST https://api.domainee.dev/v1/domains \ -H "Authorization: Bearer $DOMAINEE_API_KEY" \ -H "content-type: application/json" \ -d '{ "hostname": "shop.acme.com", "originUrl": "https://acme.fly.dev", "mode": "proxy", "keepHost": false, "redirectWww": true }' ``` ###### Body parameters | Field | Required | Default | Notes | |---|---|---|---| | `hostname` | ✅ | — | RFC 1123 hostname. Lowercased + dot-trimmed automatically. | | `originUrl` | ✅ | — | Must be `https://` or `http://`. We block private/loopback IPs (see [SSRF protection](https://domainee.dev/docs/errors#ssrf-protection)). | | `mode` | | `"proxy"` | `"proxy"` (reverse-proxy) or `"redirect"` (301/302). | | `keepHost` | | `false` | `proxy` mode only — forward original `Host` header. | | `redirectWww` | | `false` | Also serve and 301 the www variant. | | `redirectStatus` | | `301` | `301` (permanent) or `302` (temporary). | | `metadata` | | `{}` | Opaque object. We store and return it untouched. | ##### Response — `201 Created` ```json { "domain": { "id": "8f09b47c-b42f-4d14-8395-2989db76e6f8", "hostname": "shop.acme.com", "originUrl": "https://acme.fly.dev", "status": "pending", "mode": "proxy", "dnsRecords": [ { "type": "CNAME", "name": "shop.acme.com", "value": "edge.domainee.dev", "purpose": "Traffic Routing" } ] }, "warnings": [] } ``` `warnings` is always present, possibly empty. Non-fatal preflight findings (e.g. the hostname doesn't resolve yet, the origin is unreachable) appear here without blocking creation. ##### Errors | Status | Code | When | |---|---|---| | `400` | `bad_request` | Invalid hostname, invalid URL, reserved suffix | | `400` | `preflight_failed` | CAA blocks LE, origin SSRF | | `402` | `billing_required` | Free tier exhausted (50 domains or 100 GB) and no card on file | | `409` | `conflict` | Hostname already in use | ##### Idempotency Send `Idempotency-Key: ` to make retries safe — see [Idempotency](https://domainee.dev/docs/errors#idempotency). #### List domains Source: https://domainee.dev/docs/api/domains/listDomains GET /v1/domains — paginated list of every domain in your workspace. ``` GET /v1/domains ``` Returns every domain in the workspace tied to your API key. Cursor-paginated. ##### Request ```bash curl https://api.domainee.dev/v1/domains \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` ###### Query parameters | Param | Notes | |---|---| | `status` | Filter: `pending` · `verified` · `failed` · `expired` | | `hostname` | Exact-match lookup (case-insensitive). Returns at most one domain; `nextCursor` is always `null`. Saves you paging the whole workspace when you only need one domain. | | `limit` | 1–200, default 50 | | `cursor` | Pagination cursor from a previous `nextCursor` | ###### Looking up by hostname ```bash curl "https://api.domainee.dev/v1/domains?hostname=shop.acme.com" \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` Empty `domains` array if no match — never a 404. ##### Response — `200 OK` ```json { "domains": [ { "id": "8f09b47c-b42f-4d14-8395-2989db76e6f8", "hostname": "shop.acme.com", "originUrl": "https://acme.fly.dev", "status": "verified", "mode": "proxy", "monitorStatus": "active_ssl", "metadata": { "orderId": "ord_123", "tenant": "acme" }, "verifiedAt": "2026-05-05T11:39:19.406Z", "createdAt": "2026-05-05T11:39:12.884Z" } ], "nextCursor": "8f09b47c-b42f-4d14-8395-2989db76e6f8" } ``` Whatever you stored in `metadata` at create time round-trips on every list — useful for stitching domains back to your own records (workspace, tenant, customer id, etc.) without a separate lookup table. `nextCursor` is `null` once you've reached the last page. Pass it back as `?cursor=...` to get the following page. ##### The Domain object Full field reference for the objects returned in `domains[]`: | Field | Type | Notes | |---|---|---| | `id` | string | UUID. Use it on `GET`/`PATCH`/`DELETE`. | | `hostname` | string | The customer's hostname. Lowercase, RFC-1123. | | `originUrl` | string | URL we forward to (`proxy` mode) or redirect to (`redirect` mode). | | `status` | enum | `pending` · `verified` · `failed` · `expired`. Driven by DNS state. | | `mode` | enum | `proxy` (default) — reverse-proxy traffic. `redirect` — return 301/302. | | `keepHost` | boolean | `proxy` mode only: forward customer's `Host` header instead of rewriting. | | `redirectWww` | boolean | If true, also issue cert for the www variant and 301 it to canonical. | | `redirectStatus` | 301 \| 302 | HTTP code for `redirect` mode. | | `dnsRecords` | array | The records the customer must publish. | | `isResolving` | boolean | Whether DNS resolves the hostname at all. | | `pointsToEdge` | boolean | Whether DNS points to our edge IPs. | | `dnsPointedAt` | string[] | Snapshot of resolved IPs from last probe. | | `monitorStatus` | enum | Computed health. See [Monitor states](https://domainee.dev/docs/errors#monitor-states). | | `monitorMessage` | string | Human-readable explanation of `monitorStatus`. | | `metadata` | object | Whatever you sent on [Create](https://domainee.dev/docs/api/domains/createDomain), echoed back verbatim. Useful for keying domains to records on your side. | | `verifiedAt` | string \| null | When DNS first pointed at our edge. | | `createdAt` | string | ISO 8601 timestamp. | | `updatedAt` | string | ISO 8601 timestamp. | #### Get a domain Source: https://domainee.dev/docs/api/domains/getDomain GET /v1/domains/{id} — fetch one domain by id. ``` GET /v1/domains/{id} ``` Returns one domain in full. Use [List domains](https://domainee.dev/docs/api/domains/listDomains) if you don't already know the id. ##### Request ```bash curl https://api.domainee.dev/v1/domains/$DOMAIN_ID \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` ##### Response — `200 OK` ```json { "id": "8f09b47c-b42f-4d14-8395-2989db76e6f8", "workspaceId": "cb9e05b7-1bc3-470f-8955-3cd24930d25a", "hostname": "shop.acme.com", "originUrl": "https://acme.fly.dev", "status": "verified", "mode": "proxy", "keepHost": false, "redirectWww": false, "redirectStatus": 301, "dnsRecords": [ { "type": "CNAME", "name": "shop.acme.com", "value": "edge.domainee.dev", "purpose": "Traffic Routing" } ], "isResolving": true, "pointsToEdge": true, "dnsPointedAt": ["35.165.194.233", "52.39.55.154"], "monitorStatus": "active_ssl", "monitorMessage": "Domain is fully active and serving traffic.", "metadata": { "orderId": "ord_123", "tenant": "acme" }, "verifiedAt": "2026-05-05T11:39:19.406Z", "lastCheckedAt": "2026-05-05T13:10:04.000Z", "lastMonitoredAt": "2026-05-05T13:10:04.000Z", "createdAt": "2026-05-05T11:39:12.884Z", "updatedAt": "2026-05-05T11:39:19.406Z" } ``` `metadata` is whatever you sent on [Create](https://domainee.dev/docs/api/domains/createDomain) — stored verbatim and returned on every read. Use it to key Domainee records back to records on your side (tenant id, internal order id, etc.) without maintaining a separate lookup table. ##### Errors | Status | Code | When | |---|---|---| | `404` | `not_found` | The id doesn't exist or doesn't belong to your workspace | #### Update a domain Source: https://domainee.dev/docs/api/domains/updateDomain PATCH /v1/domains/{id} — change origin, mode, or routing flags. ``` PATCH /v1/domains/{id} ``` Updates any combination of: `originUrl`, `mode`, `keepHost`, `redirectWww`, `redirectStatus`, `metadata`. Hostname is **immutable** — delete and re-create if you need to change it. ##### Request ```bash curl -X PATCH https://api.domainee.dev/v1/domains/$DOMAIN_ID \ -H "Authorization: Bearer $DOMAINEE_API_KEY" \ -H "content-type: application/json" \ -d '{ "originUrl": "https://new-origin.fly.dev" }' ``` ###### Body parameters | Field | Notes | |---|---| | `originUrl` | New origin URL. Must be `https://` or `http://`. SSRF-checked. | | `mode` | `"proxy"` or `"redirect"`. | | `keepHost` | Forward original `Host` header (proxy mode). | | `redirectWww` | Also serve and 301 the www variant. | | `redirectStatus` | `301` or `302` for redirect mode. | | `metadata` | Replace the metadata object wholesale. | ##### Response — `200 OK` Returns the updated [Domain object](https://domainee.dev/docs/api/domains/getDomain). ##### Cache propagation The edge proxy caches resolver lookups for **60 seconds**. Changes to `originUrl` / `mode` / `keepHost` propagate within that window without any extra action. ##### Errors | Status | Code | When | |---|---|---| | `400` | `bad_request` | Invalid `originUrl` or unknown field | | `400` | `preflight_failed` | New `originUrl` resolved to a private/loopback IP | | `404` | `not_found` | The id doesn't exist or doesn't belong to your workspace | #### Delete a domain Source: https://domainee.dev/docs/api/domains/deleteDomain DELETE /v1/domains/{id} — stop routing a hostname. ``` DELETE /v1/domains/{id} ``` Stops traffic at our edge immediately. The customer's CNAME keeps pointing at us until they remove it on their side, but that's harmless — once a hostname has no domain row, our edge returns `404`. The domain count is decremented on the Stripe subscription in the same operation, so removing domains mid-cycle is prorated automatically. ##### Request ```bash curl -X DELETE https://api.domainee.dev/v1/domains/$DOMAIN_ID \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` ##### Response — `204 No Content` Empty body. The [`domain.deleted`](https://domainee.dev/docs/api/events/domainDeleted) webhook fires for every subscribed endpoint. ##### Errors | Status | Code | When | |---|---|---| | `404` | `not_found` | The id doesn't exist or doesn't belong to your workspace | #### Force a DNS check Source: https://domainee.dev/docs/api/domains/checkDomain POST /v1/domains/{id}/check — re-probe DNS immediately. ``` POST /v1/domains/{id}/check ``` Re-probes DNS for the hostname **right now** instead of waiting for the next monitor tick (~60s). Useful immediately after a customer publishes their CNAME, so you can flip your UI from "waiting for DNS" to "live" without polling. ##### Request ```bash curl -X POST https://api.domainee.dev/v1/domains/$DOMAIN_ID/check \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` ##### Response — `200 OK` Returns the refreshed [Domain object](https://domainee.dev/docs/api/domains/getDomain) with updated `monitorStatus`, `pointsToEdge`, `dnsPointedAt`, and `lastCheckedAt`. ```json { "id": "8f09b47c-b42f-4d14-8395-2989db76e6f8", "hostname": "shop.acme.com", "status": "verified", "monitorStatus": "active_ssl", "monitorMessage": "Domain is fully active and serving traffic.", "isResolving": true, "pointsToEdge": true, "dnsPointedAt": ["35.165.194.233", "52.39.55.154"], "lastCheckedAt": "2026-05-05T13:42:11.000Z" } ``` ##### Rate limit `60/min per API key` on this endpoint specifically — independently of the global `60/min`. See [Rate limits](https://domainee.dev/docs/errors#rate-limits). ##### Errors | Status | Code | When | |---|---|---| | `404` | `not_found` | The id doesn't exist or doesn't belong to your workspace | | `429` | `rate_limited` | Too many checks; respect `Retry-After` | ### Buy a Domain #### Buy a Domain Source: https://domainee.dev/docs/api/domain-purchases Resell domain registration to your users through one API call. We charge your card, register at the upstream registrar, fire a webhook when it's done. ``` # Pre-purchase GET /v1/domain-purchases/check?hostname=… # Buy POST /v1/domain-purchases (optionally autoConnect) GET /v1/domain-purchases (filterable list) GET /v1/domain-purchases/:id PATCH /v1/domain-purchases/:id (toggle autoRenew / cancel) # Live state at the registrar GET /v1/domain-purchases/:id/details # Connect to your edge POST /v1/domain-purchases/:id/connect # DNS GET /v1/domain-purchases/:id/dns PUT /v1/domain-purchases/:id/dns # Nameservers PUT /v1/domain-purchases/:id/nameservers # Renewal POST /v1/domain-purchases/:id/renew # Transfer out GET /v1/domain-purchases/:id/auth-code ``` The Buy-a-Domain API lets your users register custom domains without leaving your app. You call us, we charge your card, we register the hostname at the upstream registrar with the contact info you supply, and we hand you back a purchase record. ##### Who pays whom ``` your end-user → you → Domainee → registrar ``` You mark up however you want and bill your end-user out of band. **Domainee charges your workspace's saved Stripe card** off-session as soon as the purchase API call lands — no payment link, no checkout page, no user interaction. Same mechanism that handles your monthly Domainee subscription. If the registrar rejects the registration after we've taken your money, we **refund the charge automatically** and mark the purchase row as `refunded`. ##### Pricing ``` what you pay Domainee = registrar's wholesale price + $1 flat ``` Wholesale prices come from the registrar in real time. The $1 is Domainee's per-purchase operational fee (refund risk, FX, support). What you charge your end-user is entirely up to you. You can preview the exact price before committing: ```bash curl "https://api.domainee.dev/v1/domain-purchases/check?hostname=acme.com" \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` Returns the live `wholesaleCents` + `feeCents` + `totalCents`. See [Check availability + price](https://domainee.dev/docs/api/domain-purchases/checkAvailability). ##### What gets stored Each purchase writes a row to your workspace's `domain_purchases` ledger. You can list and filter that ledger any time — by hostname, by status, by date range, or by the opaque `customerReference` string you stashed at buy time. See [List purchases](https://domainee.dev/docs/api/domain-purchases/listPurchases). ##### What gets registered The hostname is registered at the upstream registrar with the `registrant` contact you supplied — the **end-user is the legal owner of record**, not you and not Domainee. WHOIS privacy is enabled by default (free). The domain registration itself is just the legal record. To actually serve content on it you have two paths: **Option 1 — `autoConnect` (one API call total).** Pass `autoConnect: { originUrl }` on the POST and we do the buy AND the edge provisioning AND set the DNS in one shot. Recommended for most flows. **Option 2 — Connect later.** Skip `autoConnect`, then later call [`POST /:id/connect`](https://domainee.dev/docs/api/domain-purchases/connectPurchase) to provision on the edge + set DNS. Useful when the user buys first and decides where to point the domain later. ##### What you can do after the purchase Acme can build a full domain management dashboard for their end-user using these endpoints — no Namecheap login required on anyone's side: - [Get live details](https://domainee.dev/docs/api/domain-purchases/getDetails) — expiry, nameservers, lock state - [List](https://domainee.dev/docs/api/domain-purchases/getDnsRecords) and [set DNS records](https://domainee.dev/docs/api/domain-purchases/setDnsRecords) — A, CNAME, MX, TXT, etc. - [Set custom nameservers](https://domainee.dev/docs/api/domain-purchases/setNameservers) — switch to Cloudflare / Vercel / Route 53 - [Renew](https://domainee.dev/docs/api/domain-purchases/renewPurchase) the registration - [Cancel (turn off auto-renew)](https://domainee.dev/docs/api/domain-purchases/patchPurchase) - [Get the auth code](https://domainee.dev/docs/api/domain-purchases/getAuthCode) for a transfer-out ##### Webhooks Four events fire across the purchase lifecycle: - [`domain_purchase.completed`](https://domainee.dev/docs/api/events) — registrar confirmed registration - [`domain_purchase.failed`](https://domainee.dev/docs/api/events) — charge failed, or registrar refused; refund applied where needed - [`domain_purchase.renewed`](https://domainee.dev/docs/api/events) — auto-renew or manual renewal succeeded - [`domain_purchase.renewal_failed`](https://domainee.dev/docs/api/events) — renewal attempt failed; domain may expire if not resolved Same signed delivery + retry behavior as the rest of Domainee. #### Check availability + price Source: https://domainee.dev/docs/api/domain-purchases/checkAvailability GET /v1/domain-purchases/check — is the hostname free, and what would it cost? ``` GET /v1/domain-purchases/check?hostname=acme.com ``` Quotes a hostname against the upstream registrar. No charge, no row written. Use this to show your end-user the price *before* they confirm. ##### Request ```bash curl "https://api.domainee.dev/v1/domain-purchases/check?hostname=janesbakery.com" \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` ###### Query parameters | Param | Required | Notes | |---|---|---| | `hostname` | ✅ | RFC 1123 hostname. Lowercased + trimmed automatically. | ##### Response — `200 OK` (available) ```json { "hostname": "janesbakery.com", "available": true, "premium": false, "pricing": { "wholesaleCents": 1418, "feeCents": 100, "totalCents": 1518, "currency": "USD" } } ``` | Field | Notes | |---|---| | `available` | `true` if the hostname can be registered right now | | `premium` | `true` if the registrar treats this as a premium-tier name (different pricing rules, see below) | | `pricing.wholesaleCents` | What the registrar charges us in cents | | `pricing.feeCents` | Domainee's flat $1 operational fee (always 100 in v1) | | `pricing.totalCents` | What we'll charge your workspace if you proceed | | `pricing.currency` | Always `USD` for v1 | ##### Response — `200 OK` (unavailable) ```json { "hostname": "google.com", "available": false, "premium": false, "pricing": null } ``` `pricing` is `null` when the hostname can't be registered (already taken, banned TLD, etc.). ##### Premium domains Some TLDs and short names carry a registry-set premium price (think `one.com`, two-letter `.io` names, etc.). When the registrar flags a name as premium, `pricing.wholesaleCents` reflects the real premium price — not the regular .com/.io tier — and the `premium` flag is `true`. **Always re-check the price right before a purchase**; premium pricing can change between the quote and the buy. ##### Notes - This endpoint is **cheap and idempotent**. Call it as often as you like from your domain-search UI. - Pricing is live from the registrar, so currency conversions, sales, and premium-name pricing all flow through here. - For comparing many hostnames in one shot, just call this in parallel. We'll add a batch variant if usage warrants it. #### Buy a domain Source: https://domainee.dev/docs/api/domain-purchases/createPurchase POST /v1/domain-purchases — charge your card and register a hostname for your end-user. ``` POST /v1/domain-purchases ``` Charges your workspace's saved Stripe card, then registers the hostname at the upstream registrar with the contact you supply. Returns the purchase record on success. Auto-refunds and reports failure if the registrar refuses after the charge succeeded. ##### Request ```bash curl -X POST https://api.domainee.dev/v1/domain-purchases \ -H "Authorization: Bearer $DOMAINEE_API_KEY" \ -H "content-type: application/json" \ -d '{ "hostname": "janesbakery.com", "years": 1, "registrant": { "firstName": "Jane", "lastName": "Smith", "email": "jane@bakery.example", "phone": "+1.5551234567", "address1": "123 Main St", "city": "Portland", "stateOrProvince": "OR", "postalCode": "97201", "country": "US" }, "customerReference": "user_jane_42" }' ``` ###### Body parameters | Field | Required | Default | Notes | |---|---|---|---| | `hostname` | ✅ | — | The domain to register. Lowercased + trimmed automatically. | | `years` | | `1` | Integer 1–10. How long to register for. | | `registrant` | ✅ | — | Contact info for the legal owner of record. See below. | | `enableWhoisPrivacy` | | `true` | Free WHOIS privacy at the registrar. Set `false` only if you need public WHOIS. | | `autoRenew` | | `false` | Renew at the registrar automatically before expiry. | | `maxTotalCents` | | — | Safety ceiling — if live price exceeds this, we return `409 price_changed` *before* charging. Recommended on every call to protect against premium-price surprises. | | `customerReference` | | — | Opaque string echoed back on reads + webhooks. Stash your internal user-id, order-id, etc. — then filter the list endpoint by it later. | | `autoConnect` | | — | When present, after registration we ALSO provision the hostname on Domainee's edge and set the registrar DNS to a single CNAME pointing at us. One API call ships a working live domain. See below. | ###### `autoConnect` object ```jsonc "autoConnect": { "originUrl": "https://janesbakery.acmesites.app", // required "keepHost": false // optional, default false } ``` | Field | Required | Notes | |---|---|---| | `originUrl` | ✅ | Where the edge proxies requests. Must be `https://`. | | `keepHost` | | Forward the customer's original `Host` header. Default `false`. | If `autoConnect` is set, the response's `purchase.connectedDomainId` field is populated with the matching `/v1/domains` row id, and the domain resolves to your app within a minute of DNS propagation. If any step in the chain fails (edge provisioning, DNS update), the **entire transaction rolls back** — the Stripe charge is refunded, the purchase row is marked `refunded`, and `domain_purchase.failed` fires. ###### Registrant object All fields are forwarded to the registrar verbatim. **Your end-user is the legal owner of record**, not you. ICANN requires the address fields to be real — fake data risks suspension of the domain. | Field | Required | Notes | |---|---|---| | `firstName` | ✅ | | | `lastName` | ✅ | | | `email` | ✅ | Registrar may send verification mail here. | | `phone` | ✅ | E.164 with dot: `+1.5551234567`. | | `address1` | ✅ | | | `address2` | | | | `city` | ✅ | | | `stateOrProvince` | ✅ | | | `postalCode` | ✅ | | | `country` | ✅ | ISO 3166-1 alpha-2 (e.g. `"US"`). | | `organization` | | Set for business-owned domains. | ##### Response — `201 Created` ```json { "purchase": { "id": "f8a0c1b9-1234-…", "workspaceId": "ws_…", "hostname": "janesbakery.com", "years": 1, "wholesaleCents": 1418, "feeCents": 100, "totalCents": 1518, "currency": "USD", "registrar": "namecheap", "registrarDomainId": "182739", "status": "completed", "registrant": { "firstName": "Jane", "lastName": "Smith", "email": "jane@bakery.example", "country": "US" }, "whoisPrivacyEnabled": true, "autoRenew": false, "expiresAt": "2027-05-17T15:42:11.000Z", "customerReference": "user_jane_42", "createdAt": "2026-05-17T15:42:09.000Z", "updatedAt": "2026-05-17T15:42:11.000Z" } } ``` ##### Errors | Code | Status | When | |---|---|---| | `unavailable` | 409 | Hostname is already registered or not available for this TLD. | | `price_changed` | 409 | Live price exceeded `maxTotalCents`. No charge made. | | `billing_required` | 402 | The Stripe charge failed — declined card, no card on file, 3DS required, insufficient funds. No registration attempted. | | `registration_failed` | 502 | We charged your card but the registrar refused. We **automatically refunded** the Stripe charge and marked the row as `refunded`. The error message contains the registrar's reason. | | `bad_request` | 400 | Validation failure — missing/malformed body. | ##### Side effects - A row is written to `domain_purchases` for your workspace. - Your workspace's Stripe card is charged the full `totalCents`. - The hostname is registered at the upstream registrar with the supplied contact as the legal owner. - One of two webhook events fires: - [`domain_purchase.completed`](https://domainee.dev/docs/api/events) — happy path - [`domain_purchase.failed`](https://domainee.dev/docs/api/events) — charge failed or registrar refused ##### Idempotency Send an `Idempotency-Key` header to dedupe retries: ``` Idempotency-Key: 8a04... ``` We cache the response for 24 hours per `workspaceId × method × path × key`. Retries with the same key return the original response without re-charging or re-registering. ##### After the purchase The domain is **registered** but not yet *configured* to serve your app's content. To make it serve traffic from your platform: 1. Update DNS at the registrar to point the hostname at Domainee's edge (one CNAME, see the Custom Domains docs). 2. Call [`POST /v1/domains`](https://domainee.dev/docs/api/domains/createDomain) with the same hostname so we mint a TLS cert and start proxying. We're considering adding an `autoConnect` field that does both in one call. [Tell us if you want it.](mailto:support@domainee.dev) #### List purchases Source: https://domainee.dev/docs/api/domain-purchases/listPurchases GET /v1/domain-purchases — paginated, filterable list of every purchase in your workspace. ``` GET /v1/domain-purchases ``` Lists every purchase tied to your workspace's API key. Cursor-paginated and filterable so you can power a customer-facing dashboard, a CRM lookup, or a support tool without keeping a parallel record. ##### Request ```bash curl https://api.domainee.dev/v1/domain-purchases \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` ###### Query parameters All optional. Filters stack. | Param | Notes | |---|---| | `hostname` | Exact-match filter (case-insensitive). | | `status` | `pending` · `completed` · `failed` · `refunded`. Repeat the param for multiple. | | `customerReference` | Match the opaque string you stashed at purchase time. | | `createdAfter` | ISO 8601 date — inclusive lower bound. | | `createdBefore` | ISO 8601 date — inclusive upper bound. | | `limit` | 1–200, default 50. | | `cursor` | Pagination cursor from a previous `nextCursor`. | ###### Useful queries **All purchases for one of your end-users**, by the reference you stored: ```bash curl "https://api.domainee.dev/v1/domain-purchases?customerReference=user_jane_42" \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` **Failed purchases this month** (support triage): ```bash curl "https://api.domainee.dev/v1/domain-purchases?status=failed&status=refunded&createdAfter=2026-05-01" \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` **Find a specific domain**: ```bash curl "https://api.domainee.dev/v1/domain-purchases?hostname=janesbakery.com" \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` ##### Response — `200 OK` ```json { "purchases": [ { "id": "f8a0c1b9-1234-…", "hostname": "janesbakery.com", "years": 1, "wholesaleCents": 1418, "feeCents": 100, "totalCents": 1518, "currency": "USD", "registrar": "namecheap", "status": "completed", "registrant": { "firstName": "Jane", "lastName": "Smith", "email": "jane@bakery.example", "country": "US" }, "whoisPrivacyEnabled": true, "autoRenew": false, "expiresAt": "2027-05-17T15:42:11.000Z", "customerReference": "user_jane_42", "createdAt": "2026-05-17T15:42:09.000Z" } ], "nextCursor": "f8a0c1b9-1234-…" } ``` `nextCursor` is `null` on the last page. ##### Notes - Always implicitly scoped to the API key's workspace. There's no workspace-jumping query — one API key, one workspace's purchases. - Failed/refunded rows are kept, not deleted. Useful for reconciliation. - The `registrant` field on the row is a snapshot taken at purchase time. Source of truth for the live contact lives at the registrar. #### Get a purchase Source: https://domainee.dev/docs/api/domain-purchases/getPurchase GET /v1/domain-purchases/:id — fetch one purchase by Domainee id. ``` GET /v1/domain-purchases/:id ``` Returns one purchase by its Domainee id. Workspace-scoped — you only ever see purchases tied to the API key's workspace. ##### Request ```bash curl https://api.domainee.dev/v1/domain-purchases/f8a0c1b9-1234-… \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` ##### Response — `200 OK` Same row shape as the create endpoint and the list endpoint: ```json { "purchase": { "id": "f8a0c1b9-1234-…", "hostname": "janesbakery.com", "years": 1, "wholesaleCents": 1418, "feeCents": 100, "totalCents": 1518, "currency": "USD", "registrar": "namecheap", "registrarDomainId": "182739", "status": "completed", "registrant": { "firstName": "Jane", "lastName": "Smith", "email": "jane@bakery.example", "country": "US" }, "whoisPrivacyEnabled": true, "autoRenew": false, "expiresAt": "2027-05-17T15:42:11.000Z", "customerReference": "user_jane_42", "createdAt": "2026-05-17T15:42:09.000Z", "updatedAt": "2026-05-17T15:42:11.000Z" } } ``` ##### Errors | Code | Status | When | |---|---|---| | `not_found` | 404 | No purchase with that id in this workspace. | #### Update a purchase (cancel / auto-renew) Source: https://domainee.dev/docs/api/domain-purchases/patchPurchase PATCH /v1/domain-purchases/:id — toggle the autoRenew preference. Effectively the "cancel" action. ``` PATCH /v1/domain-purchases/:id ``` Toggles the `autoRenew` flag on an existing purchase. Set to `false` to cancel: the domain runs out its current term and isn't renewed. Domains can't be cancelled mid-term — the registration is paid for and runs until `expiresAt`. Setting `autoRenew: false` is the practical "cancel" path (the domain expires naturally on its anniversary). ##### Request ```bash curl -X PATCH https://api.domainee.dev/v1/domain-purchases/f8a0c1b9-… \ -H "Authorization: Bearer $DOMAINEE_API_KEY" \ -H "content-type: application/json" \ -d '{ "autoRenew": false }' ``` ###### Body parameters | Field | Required | Notes | |---|---|---| | `autoRenew` | ✅ | `true` enrolls in auto-renew; `false` cancels it. | ##### Response — `200 OK` Same shape as `GET /v1/domain-purchases/:id` — the full updated purchase row. ```json { "purchase": { "id": "f8a0c1b9-1234-…", "hostname": "janesbakery.com", "autoRenew": false, "expiresAt": "2027-05-17T15:42:11.000Z", "status": "completed" } } ``` ##### Notes - Toggling has no immediate financial effect. No charge, no refund. - The auto-renew worker runs daily, picks domains expiring within 30 days where `autoRenew: true`, and renews them. Cancelling later than 30 days before expiry is the same as cancelling today. - Want a hard cancel + refund? Within the registrar's grace window (first ~5 days for .com/.net) contact support — auto-self-serve refund is on the roadmap. #### Get live details Source: https://domainee.dev/docs/api/domain-purchases/getDetails GET /v1/domain-purchases/:id/details — fresh registrar state (status, nameservers, lock). ``` GET /v1/domain-purchases/:id/details ``` Returns both the stored purchase row AND a live snapshot from the registrar. Use this to power a domain-detail page in Acme's dashboard with accurate nameserver, expiry, and lock status. ##### Request ```bash curl https://api.domainee.dev/v1/domain-purchases/f8a0c1b9-…/details \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` ##### Response — `200 OK` ```json { "purchase": { /* same shape as GET /:id */ }, "live": { "hostname": "janesbakery.com", "registrarDomainId": "182739", "status": "active", "createdAt": "2026-05-17T15:42:11.000Z", "expiresAt": "2027-05-17T15:42:11.000Z", "autoRenewAtRegistrar": false, "whoisPrivacyEnabled": true, "registrarLocked": true, "nameservers": [ "dns1.registrar-servers.com", "dns2.registrar-servers.com" ] } } ``` | Field | Notes | |---|---| | `status` | `active`, `expired`, `locked`, or `unknown`. | | `registrarLocked` | Anti-hijack lock at the registrar. On by default for new domains. | | `whoisPrivacyEnabled` | Whether the registrant contact is hidden in public WHOIS. | | `nameservers` | The current authoritative nameservers. Default points at the registrar; updated by `PUT /:id/nameservers`. | | `autoRenewAtRegistrar` | Whether the registrar will auto-renew. Domainee manages renewals itself (see [the patch endpoint](https://domainee.dev/docs/api/domain-purchases/patchPurchase)), so this is informational only. | ##### Cost This endpoint makes a live call to the registrar. It's read-only, but it's not as cheap as `GET /:id` (which only hits Mongo). Cache it on your side if you're rendering a dashboard. #### Connect a purchased domain to your app Source: https://domainee.dev/docs/api/domain-purchases/connectPurchase POST /v1/domain-purchases/:id/connect — provision on Domainee's edge + auto-set DNS in one call. ``` POST /v1/domain-purchases/:id/connect ``` After you've bought a domain (without `autoConnect`), call this to make it actually serve traffic. Two things happen in one shot: 1. The hostname is registered on Domainee's edge ([POST /v1/domains](https://domainee.dev/docs/api/domains/createDomain) is run server-side). TLS is minted on first request. 2. The registrar's DNS is replaced with one CNAME → `edge.domainee.dev` so the domain starts proxying to your origin. ##### Request ```bash curl -X POST https://api.domainee.dev/v1/domain-purchases/f8a0c1b9-…/connect \ -H "Authorization: Bearer $DOMAINEE_API_KEY" \ -d '{ "originUrl": "https://janesbakery.acmesites.app", "keepHost": false }' ``` ###### Body parameters | Field | Required | Default | Notes | |---|---|---|---| | `originUrl` | ✅ | — | Where the edge proxies requests to. Must be `https://`. | | `keepHost` | | `false` | `true` to forward the customer's original `Host` header to your origin. | ##### Response — `200 OK` Returns the purchase row with `connectedDomainId` now populated: ```json { "purchase": { "id": "f8a0c1b9-…", "hostname": "janesbakery.com", "status": "completed", "connectedDomainId": "8f09b47c-b42f-4d14-…", ... } } ``` Use `connectedDomainId` to look up the connected domain via [`GET /v1/domains/:id`](https://domainee.dev/docs/api/domains/getDomain) for status/cert/monitor details. ##### Errors | Code | Status | When | |---|---|---| | `not_found` | 404 | Purchase doesn't exist (or belongs to another workspace). | | `wrong_status` | 409 | Purchase status isn't `completed`. Only connect a fully-registered domain. | | `connect_failed` | 502 | Edge provisioning failed (origin unreachable) or DNS update failed. Nothing was charged. | ##### When to use this vs. `autoConnect` | Scenario | Use | |---|---| | You buy the domain and immediately connect it to your product | `autoConnect` on POST /v1/domain-purchases — one call. | | You bought the domain a while ago, now want to connect (or reconnect to a different origin) | This endpoint. | | You want to use the domain for something other than your app (email-only, parking, etc.) | Skip both. Manage DNS directly via [`PUT /:id/dns`](https://domainee.dev/docs/api/domain-purchases/setDnsRecords). | #### List DNS records Source: https://domainee.dev/docs/api/domain-purchases/getDnsRecords GET /v1/domain-purchases/:id/dns — current DNS records at the registrar. ``` GET /v1/domain-purchases/:id/dns ``` Returns the DNS records currently set on the domain at the registrar. Records aren't stored in Domainee — the source of truth lives at the registrar, this endpoint is a thin pass-through. ##### Request ```bash curl https://api.domainee.dev/v1/domain-purchases/f8a0c1b9-…/dns \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` ##### Response — `200 OK` ```json { "hostname": "janesbakery.com", "records": [ { "type": "CNAME", "name": "@", "value": "edge.domainee.dev", "ttl": 300 }, { "type": "TXT", "name": "@", "value": "v=spf1 include:_spf.acme.com ~all", "ttl": 3600 }, { "type": "MX", "name": "@", "value": "inbox.acme.com", "ttl": 3600, "priority": 10 } ] } ``` ###### Record fields | Field | Notes | |---|---| | `type` | `A`, `AAAA`, `CNAME`, `MX`, `TXT`, `NS`, `SRV`, `URL`, `URL301`, `FRAME`. | | `name` | `@` is the apex (e.g. `janesbakery.com`). `www` would be `www.janesbakery.com`. | | `value` | Target/value of the record. IP, hostname, or text content. | | `ttl` | Cache duration in seconds. Lower = faster propagation, higher = better cache hit. | | `priority` | MX records only. Lower = preferred. | ##### Use this for - Rendering a DNS records table in your dashboard - Validating that a previous `PUT /:id/dns` actually took effect at the registrar - Pre-loading the form when the user clicks "Edit records" Cache the response on your side if you display this on every page load — each call hits the registrar's API. #### Set DNS records Source: https://domainee.dev/docs/api/domain-purchases/setDnsRecords PUT /v1/domain-purchases/:id/dns — replace the entire DNS record set (declarative). ``` PUT /v1/domain-purchases/:id/dns ``` Replaces all DNS records at the registrar with the array you send. Declarative — what you POST is the complete state. Anything not in the payload gets removed. If you want to add a record without touching the others, first [`GET /:id/dns`](https://domainee.dev/docs/api/domain-purchases/getDnsRecords) and send the merged array back. ##### Request ```bash curl -X PUT https://api.domainee.dev/v1/domain-purchases/f8a0c1b9-…/dns \ -H "Authorization: Bearer $DOMAINEE_API_KEY" \ -H "content-type: application/json" \ -d '{ "records": [ { "type": "CNAME", "name": "@", "value": "edge.domainee.dev", "ttl": 300 }, { "type": "CNAME", "name": "www", "value": "edge.domainee.dev", "ttl": 300 }, { "type": "TXT", "name": "@", "value": "v=spf1 include:_spf.acme.com ~all" }, { "type": "MX", "name": "@", "value": "inbox.acme.com", "priority": 10 } ] }' ``` ###### Body parameters | Field | Required | Notes | |---|---|---| | `records` | ✅ | Array of record objects. Maximum 100 per call. | ###### Record object | Field | Required | Notes | |---|---|---| | `type` | ✅ | `A` · `AAAA` · `CNAME` · `MX` · `TXT` · `NS` · `SRV` · `URL` · `URL301` · `FRAME`. | | `name` | ✅ | `@` for apex, `www` for `www.`, etc. | | `value` | ✅ | Target/value. Max 2048 chars (enough for TXT/DKIM). | | `ttl` | | Seconds. 60–86400. Default 1800. | | `priority` | | Required for `MX`. 0–65535. Lower = higher priority. | ##### Response — `200 OK` ```json { "hostname": "janesbakery.com", "records": [ /* exactly what you sent */ ] } ``` ##### Errors | Code | Status | When | |---|---|---| | `not_found` | 404 | Purchase doesn't exist in this workspace. | | `bad_request` | 400 | Validation failure — missing required field, invalid type, too many records. | | Registrar error | 502 | The registrar refused (e.g. invalid CNAME at apex for TLDs that don't support it). The error message contains the registrar's reason. | ##### Notes - DNS propagation is fast at modern registrars but not instant — give a minute or two before testing. - Apex CNAME (`name: "@", type: "CNAME"`) works on Namecheap (they flatten it server-side). Some TLDs reject it — surface the error in your UI and suggest the user try an A record instead. - For a fully managed "connect this domain to my app" flow, prefer [`POST /:id/connect`](https://domainee.dev/docs/api/domain-purchases/connectPurchase) which sets the right CNAME for you in one call. #### Set nameservers Source: https://domainee.dev/docs/api/domain-purchases/setNameservers PUT /v1/domain-purchases/:id/nameservers — switch to custom NS (Cloudflare, Vercel, etc.). ``` PUT /v1/domain-purchases/:id/nameservers ``` Switches the domain's authoritative nameservers. Useful when the end-user wants to manage DNS at Cloudflare/Vercel/Route53 instead of going through us. Pass an empty array to revert to the registrar's default nameservers. **Heads-up**: setting custom nameservers makes the registrar-side DNS records (managed via [`PUT /:id/dns`](https://domainee.dev/docs/api/domain-purchases/setDnsRecords)) no longer authoritative. Whoever runs the new nameservers controls resolution. Restore the default nameservers (`{ "nameservers": [] }`) to make our DNS API meaningful again. ##### Request ```bash # Switch to Cloudflare curl -X PUT https://api.domainee.dev/v1/domain-purchases/f8a0c1b9-…/nameservers \ -H "Authorization: Bearer $DOMAINEE_API_KEY" \ -d '{ "nameservers": [ "albert.ns.cloudflare.com", "babette.ns.cloudflare.com" ] }' # Or revert to default curl -X PUT https://api.domainee.dev/v1/domain-purchases/f8a0c1b9-…/nameservers \ -H "Authorization: Bearer $DOMAINEE_API_KEY" \ -d '{ "nameservers": [] }' ``` ###### Body parameters | Field | Required | Notes | |---|---|---| | `nameservers` | ✅ | Array of hostnames. 0–13 entries. Empty = revert to default. | ##### Response — `200 OK` ```json { "hostname": "janesbakery.com", "nameservers": ["albert.ns.cloudflare.com", "babette.ns.cloudflare.com"] } ``` ##### Errors | Code | Status | When | |---|---|---| | `not_found` | 404 | Purchase doesn't exist in this workspace. | | `bad_request` | 400 | More than 13 nameservers, or invalid hostname. | | Registrar error | 502 | Registrar refused — usually one of the nameservers doesn't resolve yet. Wait a few minutes after registering them upstream. | ##### Notes - Propagation can take up to 24 hours when switching nameservers (TLD glue records). Most users see the change in 5–15 minutes. - If you switch to a custom NS host and that host doesn't know about the domain yet, you'll get an NXDOMAIN at lookup time. Register the domain at the new NS provider first. - Anti-bricking: changing nameservers on a domain that's currently connected via `POST /:id/connect` will break the connection until you re-add the CNAME at the new NS provider. Re-call `connect` to re-do the wiring or do it manually at the new provider. #### Renew a domain Source: https://domainee.dev/docs/api/domain-purchases/renewPurchase POST /v1/domain-purchases/:id/renew — extend the registration. Charges wholesale + $1. ``` POST /v1/domain-purchases/:id/renew ``` Charges your saved Stripe card `wholesale + $1` per year and extends the domain's registration at the upstream registrar. Same charge-first ordering as the initial buy: refunds automatically if the renewal fails at the registrar. ##### Request ```bash curl -X POST https://api.domainee.dev/v1/domain-purchases/f8a0c1b9-…/renew \ -H "Authorization: Bearer $DOMAINEE_API_KEY" \ -d '{ "years": 1 }' ``` ###### Body parameters | Field | Required | Notes | |---|---|---| | `years` | ✅ | Integer 1–10. How many additional years to add to the registration. | ##### Response — `200 OK` The updated purchase row with the new `expiresAt`: ```json { "purchase": { "id": "f8a0c1b9-…", "hostname": "janesbakery.com", "expiresAt": "2028-05-17T15:42:11.000Z", "status": "completed", ... } } ``` ##### Errors | Code | Status | When | |---|---|---| | `not_found` | 404 | Purchase doesn't exist in this workspace. | | `wrong_status` | 409 | Purchase status isn't `completed`. Can't renew a failed/refunded purchase. | | `billing_required` | 402 | Stripe charge failed. Same reasons as the initial buy — declined card, expired, etc. | | `renewal_failed` | 502 | Card charged, registrar refused. We've already refunded the Stripe charge. | ##### Side effects - Stripe charge for `wholesale × years + $1` is captured on the workspace. - Domain's `expiresAt` advances at the registrar. - Webhook fired: [`domain_purchase.renewed`](https://domainee.dev/docs/api/events) on success or [`domain_purchase.renewal_failed`](https://domainee.dev/docs/api/events) on failure. ##### Manual vs. auto-renew This endpoint is **manual** — Acme calls it when they want to charge for another year. There's also an automatic path: set `autoRenew: true` on a purchase (via [`PATCH /:id`](https://domainee.dev/docs/api/domain-purchases/patchPurchase) or at create time), and the background worker will call this endpoint for you 30 days before expiry. Pick the model that matches your billing relationship with your end-user: | Your model | Use | |---|---| | End-user pays you yearly via subscription | `autoRenew: true` — set it once, renewals happen automatically every year | | End-user pays per-renewal manually | Keep `autoRenew: false` and call this endpoint when they pay you | | End-user might churn — you want to gate renewal on their account status | Keep `autoRenew: false`. In your own cron, check who's active, then call this endpoint for the active ones | #### Get transfer-out auth code Source: https://domainee.dev/docs/api/domain-purchases/getAuthCode GET /v1/domain-purchases/:id/auth-code — EPP/auth code for transferring the domain to another registrar. ``` GET /v1/domain-purchases/:id/auth-code ``` Returns the EPP/auth code the end-user needs to transfer the domain to another registrar (GoDaddy, Cloudflare, wherever they want). **Sensitive** — treat the response like a password. Anyone with the auth code can move the domain out. Don't log it, don't store it, hand it to the end-user securely. ##### Request ```bash curl https://api.domainee.dev/v1/domain-purchases/f8a0c1b9-…/auth-code \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` ##### Response — `200 OK` ```json { "hostname": "janesbakery.com", "authCode": "X8k!Q2pZv9Mn" } ``` ##### Errors | Code | Status | When | |---|---|---| | `not_found` | 404 | Purchase doesn't exist in this workspace. | | Registrar error | 502 | Some TLDs require the auth code be emailed to the registrant; Namecheap may decline to surface it via API for those. Surface the registrar's error in your UI. | ##### What happens after you hand it over 1. Your end-user takes the auth code to the destination registrar. 2. They initiate a transfer-in. The destination registrar contacts our registrar to verify the code. 3. ICANN sends an email to the listed registrant asking them to approve the transfer (5-day window). 4. If approved, the domain leaves Domainee's reseller account and moves to the destination. We mark our purchase row but the underlying domain is no longer managed here. ##### Side effects - No charge. - No DB write. - The auth code is **not** cached — every call hits the registrar fresh. ##### Notes - Most registrars require the domain to be **unlocked** before transfer. Namecheap defaults new domains to locked; you'll need to add an unlock-domain helper (or have the user email support) before transfer. - This is the "escape hatch" — every customer of every SaaS deserves it. Even if you never expect anyone to transfer out, exposing this endpoint builds the trust that "I really do own this domain". ### Webhook Endpoints #### Create a webhook endpoint Source: https://domainee.dev/docs/api/webhooks/createEndpoint POST /v1/webhook-endpoints — register a URL we'll POST events to. ``` POST /v1/webhook-endpoints ``` Registers a URL we'll POST signed event payloads to. The signing secret is returned **once** in the response — store it before closing. For payload format and signature verification see the [Webhooks guide](https://domainee.dev/docs/webhooks). For event payloads see [Webhook events](https://domainee.dev/docs/api/events). ##### Request ```bash curl -X POST https://api.domainee.dev/v1/webhook-endpoints \ -H "Authorization: Bearer $DOMAINEE_API_KEY" \ -H "content-type: application/json" \ -d '{ "url": "https://your-app.com/webhooks/domainee", "events": [] }' ``` ###### Body parameters | Field | Required | Notes | |---|---|---| | `url` | ✅ | Must be HTTPS. We retry up to 6 times with exponential backoff. | | `events` | ✅ | Array of event types. Empty array `[]` = subscribe to **all**. See [Webhook events](https://domainee.dev/docs/api/events). | ##### Response — `201 Created` ```json { "endpoint": { "id": "0d8ab7f2-...", "workspaceId": "cb9e05b7-...", "url": "https://your-app.com/webhooks/domainee", "events": [], "enabled": true, "createdAt": "2026-05-05T11:39:12.884Z" }, "secret": "whsec_aBcDeF…" } ``` The `secret` is returned **only on this response** — we never echo it back. It's used to verify HMAC signatures on every incoming webhook payload. Store it in your secret manager. ##### Errors | Status | Code | When | |---|---|---| | `400` | `bad_request` | URL isn't HTTPS or unknown event type in `events` | #### List webhook endpoints Source: https://domainee.dev/docs/api/webhooks/listEndpoints GET /v1/webhook-endpoints — every endpoint in your workspace. ``` GET /v1/webhook-endpoints ``` Returns every webhook endpoint in your workspace. The `secret` is omitted from list responses (we never echo secrets twice — see [Create endpoint](https://domainee.dev/docs/api/webhooks/createEndpoint) for the one-time reveal). ##### Request ```bash curl https://api.domainee.dev/v1/webhook-endpoints \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` ##### Response — `200 OK` ```json { "endpoints": [ { "id": "0d8ab7f2-...", "workspaceId": "cb9e05b7-...", "url": "https://your-app.com/webhooks/domainee", "events": ["domain.verified", "domain.failed"], "enabled": true, "createdAt": "2026-05-05T11:39:12.884Z" } ] } ``` If you've lost the signing secret, delete the endpoint and recreate it. #### Delete a webhook endpoint Source: https://domainee.dev/docs/api/webhooks/deleteEndpoint DELETE /v1/webhook-endpoints/{id} — stop receiving events at a URL. ``` DELETE /v1/webhook-endpoints/{id} ``` Stops sending events to the endpoint. **In-flight retries are dropped** — any deliveries currently queued for the endpoint are abandoned, not finished. ##### Request ```bash curl -X DELETE https://api.domainee.dev/v1/webhook-endpoints/$ENDPOINT_ID \ -H "Authorization: Bearer $DOMAINEE_API_KEY" ``` ##### Response — `204 No Content` Empty body. ##### Errors | Status | Code | When | |---|---|---| | `404` | `not_found` | The id doesn't exist or doesn't belong to your workspace | ### Webhook Events #### Event reference Source: https://domainee.dev/docs/api/events Every event type and its payload shape. All webhook payloads share this envelope: ```json { "id": "", "type": "domain.verified", "createdAt": "2026-05-05T11:39:19.406Z", "data": { ... } } ``` The `data` shape is event-specific. ##### Event types | Type | Fires when | |---|---| | [`domain.created`](https://domainee.dev/docs/api/events/domainCreated) | `POST /v1/domains` succeeds (status `pending`). | | [`domain.verified`](https://domainee.dev/docs/api/events/domainVerified) | DNS first points at our edge. | | [`domain.failed`](https://domainee.dev/docs/api/events/domainFailed) | Pending domain stays unresolvable, or cert issuance fails irrecoverably. | | [`domain.expired`](https://domainee.dev/docs/api/events/domainExpired) | DNS stops pointing at us, or cert window elapsed. | | [`domain.monitor_updated`](https://domainee.dev/docs/api/events/domainMonitorUpdated) | Any runtime monitor signal change (noisier). | | [`domain.deleted`](https://domainee.dev/docs/api/events/domainDeleted) | `DELETE /v1/domains/{id}` succeeds. | ##### Subscribing to a subset When you create the endpoint, pass an explicit list of event types in `events`: ```json { "url": "https://your-app.com/webhooks/domainee", "events": ["domain.verified", "domain.failed", "domain.expired"] } ``` An empty `events: []` subscribes to **all** events. If you only care about lifecycle transitions, subscribe to `domain.verified` / `domain.failed` / `domain.expired` and skip `monitor_updated`. See the [Webhooks guide](https://domainee.dev/docs/webhooks) for delivery format, signature verification, and retry behavior. #### domain.created Source: https://domainee.dev/docs/api/events/domainCreated Fires when you POST /v1/domains. Fires when you successfully [`POST /v1/domains`](https://domainee.dev/docs/api/domains/createDomain). The domain status is `pending` at this point — DNS hasn't been verified yet. ##### Payload ```json { "id": "", "type": "domain.created", "createdAt": "2026-05-05T11:39:12.884Z", "data": { "id": "8f09b47c-...", "hostname": "shop.acme.com", "status": "pending" } } ``` ##### Use it for - Provisioning rows in your own DB - Kicking off a follow-up email to your customer with DNS instructions #### domain.verified Source: https://domainee.dev/docs/api/events/domainVerified Fires when DNS first points at our edge. Fires when DNS for the hostname starts pointing at our edge IPs. This is the moment your customer's site goes live (modulo first-request cert issuance, which takes 10–30s). ##### Payload ```json { "id": "", "type": "domain.verified", "createdAt": "2026-05-05T11:39:19.406Z", "data": { "id": "8f09b47c-...", "hostname": "shop.acme.com", "status": "verified" } } ``` ##### Use it for - Marking the domain "live" in your customer's UI - Sending a "your domain is now active" email - Kicking off post-launch tasks (e.g. updating internal records) #### domain.failed Source: https://domainee.dev/docs/api/events/domainFailed Fires when a pending domain gives up or cert issuance fails irrecoverably. Fires if a `pending` domain stays unresolvable past a threshold (about 7 days of failed checks), or if cert issuance failed irrecoverably (e.g. CAA records that don't authorize Let's Encrypt). ##### Payload ```json { "id": "", "type": "domain.failed", "createdAt": "2026-05-12T11:39:19.406Z", "data": { "id": "8f09b47c-...", "hostname": "shop.acme.com", "status": "failed" } } ``` ##### Use it for - Re-prompting the customer to fix their DNS - Escalating to support - Surfacing the corresponding `monitorMessage` in your customer's UI See [Monitor states](https://domainee.dev/docs/errors#monitor-states) and [CAA records](https://domainee.dev/docs/errors#caa-records) for what specifically went wrong. #### domain.expired Source: https://domainee.dev/docs/api/events/domainExpired Fires when a previously verified domain stops pointing at us, or its cert lapsed. Fires if a previously `verified` domain's DNS stops pointing at us OR its cert window has elapsed without renewal. ##### Payload ```json { "id": "", "type": "domain.expired", "createdAt": "2026-08-03T11:39:19.406Z", "data": { "id": "8f09b47c-...", "hostname": "shop.acme.com", "status": "expired" } } ``` ##### Use it for - Alerting the customer their site is down - Retrying renewal - Switching your UI from "live" back to "needs DNS" #### domain.monitor_updated Source: https://domainee.dev/docs/api/events/domainMonitorUpdated Fires whenever the runtime monitor signal changes — DNS shifts, SSL state changes, etc. Fires whenever the runtime monitor signal changes — DNS started/stopped resolving, switched away from our edge IPs, SSL state shifted, etc. This event is **noisier** than the other lifecycle events: it can fire even when the overall `status` doesn't change. If you only care about lifecycle transitions, subscribe to `domain.verified` / `domain.failed` / `domain.expired` instead. ##### Payload ```json { "id": "", "type": "domain.monitor_updated", "createdAt": "2026-05-05T13:10:04.000Z", "data": { "id": "8f09b47c-...", "hostname": "shop.acme.com", "monitorStatus": "active_ssl", "monitorMessage": "Domain is fully active and serving traffic.", "isResolving": true, "pointsToEdge": true, "dnsPointedAt": ["35.165.194.233", "52.39.55.154"], "sslActiveFrom": "2026-05-05T11:39:00.000Z", "sslActiveUntil": "2026-08-03T11:39:00.000Z" } } ``` See [Monitor states](https://domainee.dev/docs/errors#monitor-states) for the full enumeration of `monitorStatus` values. ##### Use it for - Live status badges in your dashboard - Alerting on `dns_incorrect` or `target_not_loading` - Showing the SSL renewal window in your customer's UI #### domain.deleted Source: https://domainee.dev/docs/api/events/domainDeleted Fires when you DELETE /v1/domains/{id}. Fires when you [`DELETE /v1/domains/{id}`](https://domainee.dev/docs/api/domains/deleteDomain). The domain is already gone by the time the event fires — this is a confirmation hook, not a "you can still cancel" hook. ##### Payload ```json { "id": "", "type": "domain.deleted", "createdAt": "2026-05-05T14:00:00.000Z", "data": { "id": "8f09b47c-...", "hostname": "shop.acme.com" } } ``` ##### Use it for - Cleaning up corresponding rows on your side - Double-entry auditing - Removing the domain from any internal status pages #### domain_purchase.completed Source: https://domainee.dev/docs/api/events/domainPurchaseCompleted Fires after a successful POST /v1/domain-purchases. Fires when a [purchase](https://domainee.dev/docs/api/domain-purchases/createPurchase) has been charged AND registered at the upstream registrar. By the time you receive this, the domain is legally registered to your end-user. ##### Payload ```json { "id": "", "type": "domain_purchase.completed", "createdAt": "2026-05-17T15:42:11.000Z", "data": { "purchase": { "id": "f8a0c1b9-1234-…", "hostname": "janesbakery.com", "years": 1, "wholesaleCents": 1418, "feeCents": 100, "totalCents": 1518, "currency": "USD", "registrar": "namecheap", "registrarDomainId": "182739", "status": "completed", "registrant": { "firstName": "Jane", "lastName": "Smith", "email": "jane@bakery.example", "country": "US" }, "whoisPrivacyEnabled": true, "autoRenew": false, "expiresAt": "2027-05-17T15:42:11.000Z", "customerReference": "user_jane_42", "createdAt": "2026-05-17T15:42:09.000Z", "updatedAt": "2026-05-17T15:42:11.000Z" } } } ``` ##### Use it for - Sending a confirmation email to your end-user - Provisioning the domain in your app (DNS, hostname routing) - Calling [`POST /v1/domains`](https://domainee.dev/docs/api/domains/createDomain) to start proxying the hostname through Domainee's edge - Updating your own ledger / invoice / receipts #### domain_purchase.failed Source: https://domainee.dev/docs/api/events/domainPurchaseFailed Fires when a purchase is rejected or the registrar refuses after charging. Fires when a [purchase](https://domainee.dev/docs/api/domain-purchases/createPurchase) couldn't be completed. There are two scenarios: 1. **Charge failed** — your workspace card declined / required 3DS / had no funds. Status: `failed`. No registration attempted. No refund needed. 2. **Registrar refused after the charge succeeded** — we charged your card, the registrar then rejected the registration. We **automatically refunded** the Stripe charge. Status: `refunded`. ##### Payload ```json { "id": "", "type": "domain_purchase.failed", "createdAt": "2026-05-17T15:42:11.000Z", "data": { "purchase": { "id": "f8a0c1b9-1234-…", "hostname": "janesbakery.com", "years": 1, "totalCents": 1518, "currency": "USD", "status": "refunded", "errorMessage": "Namecheap API error: Insufficient funds in reseller account", "stripePaymentIntentId": "pi_…", "stripeRefundId": "re_…", "customerReference": "user_jane_42", "createdAt": "2026-05-17T15:42:09.000Z", "updatedAt": "2026-05-17T15:42:12.000Z" }, "error": "Namecheap API error: Insufficient funds in reseller account" } } ``` | Field | Notes | |---|---| | `data.purchase.status` | `failed` (no charge) or `refunded` (charge made + refunded) | | `data.purchase.errorMessage` | Registrar-side reason. Plain text, safe to surface to your support tooling. | | `data.purchase.stripeRefundId` | Present when the registrar failed after the charge. Look up in Stripe for full refund details. | | `data.error` | Same as `errorMessage`, top-level for convenience. | ##### Use it for - Notifying your end-user that the registration didn't go through - Reversing any optimistic UI state in your app - Logging into your own support queue with the registrar's error message - Surfacing refund status in your own dashboard ##### What you *don't* need to do - Issue your own refund — it's already done. If your end-user paid you out-of-band, that's between you and them. - Retry the call — for the same hostname, retry will hit the same failure. Resolve the underlying issue (card, funds, premium price) and try again. #### domain_purchase.renewed Source: https://domainee.dev/docs/api/events/domainPurchaseRenewed Fires after a successful renewal (manual or auto). Fires when a purchase has been renewed at the registrar — either via an explicit [`POST /:id/renew`](https://domainee.dev/docs/api/domain-purchases/renewPurchase) call or by the daily auto-renew worker. The Stripe charge has succeeded, the registrar has confirmed the renewal, and the purchase row's `expiresAt` has been updated. ##### Payload ```json { "id": "", "type": "domain_purchase.renewed", "createdAt": "2027-04-17T15:42:11.000Z", "data": { "purchase": { /* updated purchase row, new expiresAt */ }, "years": 1, "chargedCents": 1518, "paymentIntentId": "pi_…" } } ``` ##### Use it for - Sending a "your domain just renewed" email to the end-user - Updating your own ledger / invoice / receipt - Refreshing the expiry date cached in your dashboard #### domain_purchase.renewal_failed Source: https://domainee.dev/docs/api/events/domainPurchaseRenewalFailed Fires when a renewal attempt fails. Domain may still expire. Fires when a renewal attempt failed — either the Stripe charge declined, or the registrar refused after we'd charged (in which case we automatically refunded). If this fires within the renewal window (30 days before expiry) and isn't resolved, the domain will expire on its `expiresAt` date. ##### Payload ```json { "id": "", "type": "domain_purchase.renewal_failed", "createdAt": "2027-04-17T15:42:11.000Z", "data": { "purchase": { /* unchanged purchase row */ }, "error": "Payment failed: Your card was declined." } } ``` | Field | Notes | |---|---| | `data.purchase` | The purchase row, unchanged (the renewal didn't happen). | | `data.error` | Why it failed. Plain text, safe to surface to your support tooling. | ##### Common failure modes | Error contains | What to do | |---|---| | `Your card was declined` | Acme's saved Stripe card needs updating. Email Acme. | | `Insufficient funds` (in Namecheap response) | Domainee's Namecheap balance is low. Internal issue — top up. | | `domain is locked` | Rare. The domain is registrar-locked in a way that prevents renewal. Contact support. | ##### Use it for - Email Acme: "Your card was declined, the next renewal won't go through" - Email the end-user: "Your domain janesbakery.com will expire on X. Update your payment method" - Mark the workspace in your CRM for billing follow-up - Disable any features tied to a soon-to-expire custom domain ##### Auto-retry The worker doesn't retry automatically — once a renewal_failed fires, nothing else happens until either Acme manually calls [`POST /:id/renew`](https://domainee.dev/docs/api/domain-purchases/renewPurchase) or the domain expires. Build your own retry logic if you need it. ### DNS Checks #### Check records exist Source: https://domainee.dev/docs/api/dns/checkRecordsExist POST /v1/dns/check-records-exist — DNS lookup with "any match" semantics. ``` POST /v1/dns/check-records-exist ``` For each record entry, returns `match: true` if **at least one** of the values returned by DNS matches `match_against`. Use this when you want to confirm the customer published the right record without rejecting them for also having other records alongside it. If you need the stricter "no other records" semantics, see [Check records match exactly](https://domainee.dev/docs/api/dns/checkRecordsMatchExactly). ##### Request ```bash curl -X POST https://api.domainee.dev/v1/dns/check-records-exist \ -H "Authorization: Bearer $DOMAINEE_API_KEY" \ -H "content-type: application/json" \ -d '{ "records": [ { "address": "shop.acme.com", "type": "cname", "match_against": "edge.domainee.dev" } ] }' ``` ###### Body parameters | Field | Notes | |---|---| | `records[]` | Up to **50 entries** per request. | | `records[].address` | The hostname to look up. | | `records[].type` | One of: `a`, `aaaa`, `cname`, `mx`, `txt`, `ns`, `caa`. | | `records[].match_against` | The expected value. Comparison is case-insensitive; trailing dots stripped. | ##### Response — `200 OK` ```json { "records": [ { "address": "shop.acme.com", "type": "cname", "match_against": "edge.domainee.dev", "match": true, "actual_values": ["edge.domainee.dev"] } ] } ``` ##### Supported record types | `type` | Looks up | Notes | |---|---|---| | `a` | A records | IPv4 | | `aaaa` | AAAA records | IPv6 | | `cname` | CNAME | Doesn't follow the chain — returns the immediate CNAME target | | `mx` | MX records | Format: `" "` | | `txt` | TXT records | Each TXT chunk is concatenated into a single string per record | | `ns` | NS records | | | `caa` | CAA records | Format: `"issue "` / `"issuewild "` / `"iodef "` | ##### Rate limit `600/hour per workspace`. See [Rate limits](https://domainee.dev/docs/errors#rate-limits). Batch as many checks as you can into a single call (up to 50) to stay within the limit. #### Check records match exactly Source: https://domainee.dev/docs/api/dns/checkRecordsMatchExactly POST /v1/dns/check-records-match-exactly — DNS lookup with strict "no other values" semantics. ``` POST /v1/dns/check-records-match-exactly ``` Same request shape as [Check records exist](https://domainee.dev/docs/api/dns/checkRecordsExist), with stricter rules: returns `match: true` only if **every** value DNS returned equals `match_against`. Useful when the customer must point ONLY at us — for example, they shouldn't also have an A record alongside the CNAME we asked for. ##### Request ```bash curl -X POST https://api.domainee.dev/v1/dns/check-records-match-exactly \ -H "Authorization: Bearer $DOMAINEE_API_KEY" \ -H "content-type: application/json" \ -d '{ "records": [ { "address": "shop.acme.com", "type": "cname", "match_against": "edge.domainee.dev" } ] }' ``` ###### Body parameters Same as [Check records exist](https://domainee.dev/docs/api/dns/checkRecordsExist#body-parameters). ##### Response — `200 OK` ```json { "records": [ { "address": "shop.acme.com", "type": "cname", "match_against": "edge.domainee.dev", "match": true, "actual_values": ["edge.domainee.dev"] } ] } ``` `match` is `false` if DNS returned anything that *doesn't* equal `match_against`, even if `match_against` was *also* in the result set. ##### Rate limit `600/hour per workspace`. See [Rate limits](https://domainee.dev/docs/errors#rate-limits). Batch up to 50 checks per call. ## Free APIs ### Free Public APIs Source: https://domainee.dev/docs/free-apis REST APIs for DNS, SSL, WHOIS, email auth, and HTTP — free, no API key, CORS-enabled. Domainee's free public APIs are the same lookups behind our [visual free tools](https://domainee.dev/free-tools), exposed as a clean REST surface. **No API key. No signup.** Just call the endpoint. #### Base URL ``` https://api.domainee.dev ``` All free endpoints live under `/v1/tools/`. #### Authentication None. Every endpoint under `/v1/tools/*` accepts anonymous requests from any origin. If you need higher rate limits, custom-domain provisioning, or AI-agent control via MCP, [sign up for free](https://domainee.dev/sign-up) and use the authenticated endpoints under `/v1/domains` and friends. #### Response envelope Every response uses a stable envelope so you can write one handler that works for all of them. **Success**: ```json { "ok": true, "data": { /* endpoint-specific payload */ } } ``` **Error**: ```json { "ok": false, "error": { "code": "invalid_host", "message": "Enter a valid hostname (e.g. example.com or example.com:443)" } } ``` `code` is stable and safe to switch on. `message` is human-friendly and may evolve. Don't parse it. #### Rate limits Per-IP, with two windows: | Window | Limit | |---|---| | 60 seconds | 30 requests | | 24 hours | 500 requests | Exceeding either returns **HTTP 429** with: - A `Retry-After` header (seconds until you can retry) - An `error.code` of `"rate_limited"` - A `message` explaining which window you hit The response also includes `X-RateLimit-Remaining-Minute` and `X-RateLimit-Remaining-Day` headers on successful requests so you can back off proactively. #### CORS `Access-Control-Allow-Origin: *` on every response. Safe to call from browser JavaScript without a proxy. ```js // Works straight from your front-end: const res = await fetch( "https://api.domainee.dev/v1/tools/ssl-check?host=example.com", ); const { ok, data } = await res.json(); ``` #### Endpoints | Endpoint | What it does | |---|---| | [`/v1/tools/ssl-check`](https://domainee.dev/docs/free-apis/ssl-certificate-checker) | TLS cert details + chain for any hostname | More endpoints are rolling out — see the [Free APIs index](https://domainee.dev/free-apis) for the up-to-date list. #### Caching Successful responses set `Cache-Control: public, max-age=60`. Reasonable to cache for a minute on your end. DNS data churns slowly; aggressive caching is fine. #### Best-effort SLA These are free APIs. They run on the same edge as the rest of Domainee and we treat them as production quality, but we make no formal SLA. If your business depends on uptime, talk to us at [hello@domainee.dev](mailto:hello@domainee.dev) about a paid tier. ### SSL Certificate Checker API Reference Source: https://domainee.dev/docs/free-apis/ssl-certificate-checker Endpoint reference for GET /v1/tools/ssl-check: parameters, JSON response schema, error codes, and per-IP rate limits. No API key required. The SSL Certificate Checker API connects to any public HTTPS host, performs a real TLS handshake, and returns structured info about the certificate the server presented. Useful for cert monitoring, expiry dashboards, debugging chain issues, and validating SAN coverage on custom domains. ``` GET https://api.domainee.dev/v1/tools/ssl-check?host=domainee.dev ``` Returns TLS certificate details for any public hostname: subject, issuer, validity, alt names, cipher, full chain. Free. No API key required. CORS-enabled. Rate-limited per IP at 30/min and 500/day. Parameters: | Name | In | Type | Required | Description | Example | |---|---|---|---|---|---| | `host` | query | string | yes | Hostname to check. Optionally with `:port` (default 443). | `domainee.dev` | Example request: ```bash curl -s "https://api.domainee.dev/v1/tools/ssl-check?host=domainee.dev" | jq ``` Example response: ```json { "ok": true, "data": { "host": "domainee.dev", "port": 443, "protocol": "TLSv1.3", "cipher": { "name": "TLS_AES_256_GCM_SHA384", "version": "TLSv1.3" }, "authorized": true, "expired": false, "daysUntilExpiry": 71, "subject": { "CN": "domainee.dev" }, "issuer": { "C": "US", "O": "Let's Encrypt", "CN": "E7" }, "validFrom": "2026-03-15T08:32:14.000Z", "validTo": "2026-06-13T08:32:13.000Z", "altNames": [ "domainee.dev", "*.domainee.dev" ] } } ``` #### Notes - **No hostname allowlist.** You can check any public hostname. - **Private, loopback, link-local, and reserved IPs are blocked** to prevent SSRF (`forbidden_host` error). - **Connection timeout: 8 seconds.** For non-responsive hosts you'll get `tls_connection_failed` after that window. - **Certificate validation is intentionally lenient** — we read invalid certs too, so you can REPORT them as invalid instead of erroring out. Check the `authorized` and `authorizationError` fields to know whether the cert chain validates. #### Common use cases - **Cert expiry monitoring.** Run this once a day per hostname, alert when `daysUntilExpiry < 14`. Same data Let's Encrypt and ACME internal tooling uses. - **Chain debugging.** When customers report mixed-content or intermediate-cert issues, the `chain` field shows exactly what the server presented (or didn't). - **SAN coverage check.** Make sure `altNames` includes the apex AND the `www` variant before pushing a DNS cutover. #### See also - [SSL Certificate Checker visual tool](https://domainee.dev/free-tools/ssl-certificate-checker) — UI version that hits this same API. - [Domainee Automatic SSL](https://domainee.dev/ssl) — if you want certs automatically issued and renewed for your customers' custom domains. ### DNS Record Lookup API Reference Source: https://domainee.dev/docs/free-apis/dns-record-lookup Endpoint reference for GET /v1/tools/dns-record-lookup: parameters, JSON response schema, error codes, and per-IP rate limits. No API key required. ``` GET https://api.domainee.dev/v1/tools/dns-record-lookup?domain=domainee.dev ``` Look up DNS records for any hostname. Returns all common record types (A, AAAA, CNAME, MX, TXT, NS, SOA) by default, or only the type you specify. Free. No API key required. CORS-enabled. Rate-limited per IP at 30/min and 500/day. Parameters: | Name | In | Type | Required | Description | Example | |---|---|---|---|---|---| | `domain` | query | string | yes | Domain or hostname to query. | `domainee.dev` | | `type` | query | A \| AAAA \| CNAME \| MX \| TXT \| NS \| SOA | no | Restrict to one record type. Defaults to all common types. | — | Example request: ```bash curl -s "https://api.domainee.dev/v1/tools/dns-record-lookup?domain=domainee.dev" | jq ``` Example response: ```json { "ok": true, "data": { "domain": "domainee.dev", "records": [ { "type": "A", "values": [ "35.165.194.233", "52.39.55.154" ] }, { "type": "MX", "values": [ "10 mail.example.com." ] }, { "type": "NS", "values": [ "abdullah.ns.cloudflare.com.", "audrey.ns.cloudflare.com." ] } ] } } ``` #### See also - [DNS Record Lookup visual tool](https://domainee.dev/free-tools/dns-record-lookup) — UI version that hits this same API. - [Free APIs overview](https://domainee.dev/docs/free-apis) — rate limits, response envelope, error codes. ### WHOIS Lookup API Reference Source: https://domainee.dev/docs/free-apis/whois-lookup Endpoint reference for GET /v1/tools/whois-lookup: parameters, JSON response schema, error codes, and per-IP rate limits. No API key required. ``` GET https://api.domainee.dev/v1/tools/whois-lookup?domain=domainee.dev ``` Modern RDAP-backed WHOIS lookup. Returns registrar, status, key dates (created/updated/expires), nameservers, and registrant info where public. Free. No API key required. CORS-enabled. Rate-limited per IP at 30/min and 500/day. Parameters: | Name | In | Type | Required | Description | Example | |---|---|---|---|---|---| | `domain` | query | string | yes | Domain to look up. | `domainee.dev` | Example request: ```bash curl -s "https://api.domainee.dev/v1/tools/whois-lookup?domain=domainee.dev" | jq ``` Example response: ```json { "ok": true, "data": { "domain": "domainee.dev", "registrar": "Namecheap, Inc.", "status": [ "clientTransferProhibited" ], "created": "2025-09-12T08:21:33.000Z", "updated": "2026-03-04T11:08:12.000Z", "expires": "2027-09-12T08:21:33.000Z", "nameServers": [ "abdullah.ns.cloudflare.com", "audrey.ns.cloudflare.com" ] } } ``` #### See also - [WHOIS Lookup visual tool](https://domainee.dev/free-tools/whois-lookup) — UI version that hits this same API. - [Free APIs overview](https://domainee.dev/docs/free-apis) — rate limits, response envelope, error codes. ### CNAME Lookup & Generator API Reference Source: https://domainee.dev/docs/free-apis/cname-lookup Endpoint reference for GET /v1/tools/cname-lookup: parameters, JSON response schema, error codes, and per-IP rate limits. No API key required. ``` GET https://api.domainee.dev/v1/tools/cname-lookup?host=www.domainee.dev ``` Resolve a hostname's CNAME chain. Flags apex-domain misuse (apex domains can't legally have CNAME records per RFC 1034) and returns the full chain to the final A record. Free. No API key required. CORS-enabled. Rate-limited per IP at 30/min and 500/day. Parameters: | Name | In | Type | Required | Description | Example | |---|---|---|---|---|---| | `host` | query | string | yes | Hostname to resolve. Use a subdomain like `www.example.com`. | `www.domainee.dev` | Example request: ```bash curl -s "https://api.domainee.dev/v1/tools/cname-lookup?host=www.domainee.dev" | jq ``` Example response: ```json { "ok": true, "data": { "host": "www.domainee.dev", "isApex": false, "cnames": [ "domainee.dev." ], "finalIps": [ "35.165.194.233" ] } } ``` #### See also - [CNAME Lookup visual tool](https://domainee.dev/free-tools/cname-lookup) — UI version that hits this same API. - [Free APIs overview](https://domainee.dev/docs/free-apis) — rate limits, response envelope, error codes. ### HTTP Header Checker API Reference Source: https://domainee.dev/docs/free-apis/http-header-checker Endpoint reference for GET /v1/tools/http-header-checker: parameters, JSON response schema, error codes, and per-IP rate limits. No API key required. ``` GET https://api.domainee.dev/v1/tools/http-header-checker?url=https%3A%2F%2Fdomainee.dev ``` Fetch a URL and return its response headers + a security grade based on HSTS, CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and Permissions-Policy presence. Free. No API key required. CORS-enabled. Rate-limited per IP at 30/min and 500/day. Parameters: | Name | In | Type | Required | Description | Example | |---|---|---|---|---|---| | `url` | query | string | yes | Full URL to fetch (must include scheme). | `https://domainee.dev` | Example request: ```bash curl -s "https://api.domainee.dev/v1/tools/http-header-checker?url=https://domainee.dev" | jq ``` Example response: ```json { "ok": true, "data": { "url": "https://domainee.dev", "status": 200, "statusText": "OK", "headers": { "server": "Caddy", "content-type": "text/html" }, "securityHeaders": [ { "name": "strict-transport-security", "present": true }, { "name": "content-security-policy", "present": false } ], "grade": "B" } } ``` #### See also - [HTTP Header Checker visual tool](https://domainee.dev/free-tools/http-header-checker) — UI version that hits this same API. - [Free APIs overview](https://domainee.dev/docs/free-apis) — rate limits, response envelope, error codes. ### DNS Propagation Checker API Reference Source: https://domainee.dev/docs/free-apis/dns-propagation-checker Endpoint reference for GET /v1/tools/dns-propagation-checker: parameters, JSON response schema, error codes, and per-IP rate limits. No API key required. ``` GET https://api.domainee.dev/v1/tools/dns-propagation-checker?host=domainee.dev ``` Query the same hostname against ~10 public resolvers across the world (Cloudflare, Google, Quad9, OpenDNS, etc.) and report which ones see the same answer. Useful right after a DNS change. Free. No API key required. CORS-enabled. Rate-limited per IP at 30/min and 500/day. Parameters: | Name | In | Type | Required | Description | Example | |---|---|---|---|---|---| | `host` | query | string | yes | Hostname to query across resolvers. | `domainee.dev` | | `type` | query | A \| AAAA \| CNAME \| MX \| TXT \| NS | no | Record type to check. Defaults to A. | — | Example request: ```bash curl -s "https://api.domainee.dev/v1/tools/dns-propagation-checker?host=domainee.dev&type=A" | jq ``` Example response: ```json { "ok": true, "data": { "domain": "domainee.dev", "type": "A", "consistent": true, "results": [ { "resolver": "Cloudflare (1.1.1.1)", "values": [ "35.165.194.233" ] }, { "resolver": "Google (8.8.8.8)", "values": [ "35.165.194.233" ] } ] } } ``` #### See also - [DNS Propagation Checker visual tool](https://domainee.dev/free-tools/dns-propagation-checker) — UI version that hits this same API. - [Free APIs overview](https://domainee.dev/docs/free-apis) — rate limits, response envelope, error codes. ### Redirect Checker API Reference Source: https://domainee.dev/docs/free-apis/redirect-checker Endpoint reference for GET /v1/tools/redirect-checker: parameters, JSON response schema, error codes, and per-IP rate limits. No API key required. ``` GET https://api.domainee.dev/v1/tools/redirect-checker?url=https%3A%2F%2Fdomainee.com ``` Follow a URL's redirect chain and return every hop with status code, response time, and the final destination URL. Stops at 20 hops to prevent loops. Free. No API key required. CORS-enabled. Rate-limited per IP at 30/min and 500/day. Parameters: | Name | In | Type | Required | Description | Example | |---|---|---|---|---|---| | `url` | query | string | yes | Starting URL. | `https://domainee.com` | Example request: ```bash curl -s "https://api.domainee.dev/v1/tools/redirect-checker?url=https://domainee.com" | jq ``` Example response: ```json { "ok": true, "data": { "hops": [ { "url": "https://domainee.com/", "status": 301, "statusText": "Moved Permanently", "responseTimeMs": 42 }, { "url": "https://domainee.dev/", "status": 200, "statusText": "OK", "responseTimeMs": 65 } ], "finalUrl": "https://domainee.dev/" } } ``` #### See also - [Redirect Checker visual tool](https://domainee.dev/free-tools/redirect-checker) — UI version that hits this same API. - [Free APIs overview](https://domainee.dev/docs/free-apis) — rate limits, response envelope, error codes. ### SPF Record Checker API Reference Source: https://domainee.dev/docs/free-apis/spf-record-checker Endpoint reference for GET /v1/tools/spf-record-checker: parameters, JSON response schema, error codes, and per-IP rate limits. No API key required. ``` GET https://api.domainee.dev/v1/tools/spf-record-checker?domain=domainee.dev ``` Look up the SPF record for a domain, validate syntax, and count void/total DNS lookups against the RFC 7208 limit of 10. Flags common issues. Free. No API key required. CORS-enabled. Rate-limited per IP at 30/min and 500/day. Parameters: | Name | In | Type | Required | Description | Example | |---|---|---|---|---|---| | `domain` | query | string | yes | Domain to check the SPF record for. | `domainee.dev` | Example request: ```bash curl -s "https://api.domainee.dev/v1/tools/spf-record-checker?domain=domainee.dev" | jq ``` Example response: ```json { "ok": true, "data": { "domain": "domainee.dev", "record": "v=spf1 include:_spf.google.com ~all", "mechanisms": [ { "kind": "include", "value": "_spf.google.com" }, { "kind": "all", "qualifier": "~" } ], "lookupCount": 4, "withinLimit": true, "issues": [] } } ``` #### See also - [SPF Record Checker visual tool](https://domainee.dev/free-tools/spf-record-checker) — UI version that hits this same API. - [Free APIs overview](https://domainee.dev/docs/free-apis) — rate limits, response envelope, error codes. ### Domain Age Checker API Reference Source: https://domainee.dev/docs/free-apis/domain-age-checker Endpoint reference for GET /v1/tools/domain-age-checker: parameters, JSON response schema, error codes, and per-IP rate limits. No API key required. ``` GET https://api.domainee.dev/v1/tools/domain-age-checker?domain=google.com ``` RDAP-backed lookup that returns a domain's registration date, age in days + years, last-updated date, and expiration date. Free. No API key required. CORS-enabled. Rate-limited per IP at 30/min and 500/day. Parameters: | Name | In | Type | Required | Description | Example | |---|---|---|---|---|---| | `domain` | query | string | yes | Domain to check. | `google.com` | Example request: ```bash curl -s "https://api.domainee.dev/v1/tools/domain-age-checker?domain=google.com" | jq ``` Example response: ```json { "ok": true, "data": { "domain": "google.com", "created": "1997-09-15T04:00:00.000Z", "updated": "2025-09-09T15:39:04.000Z", "expires": "2028-09-14T04:00:00.000Z", "ageDays": 10481, "ageYears": 28.7 } } ``` #### See also - [Domain Age Checker visual tool](https://domainee.dev/free-tools/domain-age-checker) — UI version that hits this same API. - [Free APIs overview](https://domainee.dev/docs/free-apis) — rate limits, response envelope, error codes. ### Subdomain Finder API Reference Source: https://domainee.dev/docs/free-apis/subdomain-finder Endpoint reference for GET /v1/tools/subdomain-finder: parameters, JSON response schema, error codes, and per-IP rate limits. No API key required. ``` GET https://api.domainee.dev/v1/tools/subdomain-finder?domain=domainee.dev ``` Enumerate subdomains for a target domain by querying Certificate Transparency logs (crt.sh) and DNS resolution. Returns deduplicated, sorted list. Free. No API key required. CORS-enabled. Rate-limited per IP at 30/min and 500/day. Parameters: | Name | In | Type | Required | Description | Example | |---|---|---|---|---|---| | `domain` | query | string | yes | Apex domain to enumerate subdomains for. | `domainee.dev` | Example request: ```bash curl -s "https://api.domainee.dev/v1/tools/subdomain-finder?domain=domainee.dev" | jq ``` Example response: ```json { "ok": true, "data": { "domain": "domainee.dev", "subdomains": [ "api.domainee.dev", "edge.domainee.dev", "mcp.domainee.dev", "www.domainee.dev" ], "count": 4, "source": "certificate-transparency" } } ``` #### See also - [Subdomain Finder visual tool](https://domainee.dev/free-tools/subdomain-finder) — UI version that hits this same API. - [Free APIs overview](https://domainee.dev/docs/free-apis) — rate limits, response envelope, error codes. ### Reverse IP Lookup API Reference Source: https://domainee.dev/docs/free-apis/reverse-ip-lookup Endpoint reference for GET /v1/tools/reverse-ip-lookup: parameters, JSON response schema, error codes, and per-IP rate limits. No API key required. ``` GET https://api.domainee.dev/v1/tools/reverse-ip-lookup?ip=8.8.8.8 ``` Resolve PTR (reverse DNS) records for an IP address. Returns one or more hostnames if any are configured; empty array otherwise. Private and reserved IPs are rejected. Free. No API key required. CORS-enabled. Rate-limited per IP at 30/min and 500/day. Parameters: | Name | In | Type | Required | Description | Example | |---|---|---|---|---|---| | `ip` | query | string | yes | IPv4 or IPv6 address. | `8.8.8.8` | Example request: ```bash curl -s "https://api.domainee.dev/v1/tools/reverse-ip-lookup?ip=8.8.8.8" | jq ``` Example response: ```json { "ok": true, "data": { "ip": "8.8.8.8", "hostnames": [ "dns.google" ] } } ``` #### See also - [Reverse IP Lookup visual tool](https://domainee.dev/free-tools/reverse-ip-lookup) — UI version that hits this same API. - [Free APIs overview](https://domainee.dev/docs/free-apis) — rate limits, response envelope, error codes. ### DMARC Record Checker API Reference Source: https://domainee.dev/docs/free-apis/dmarc-record-checker Endpoint reference for GET /v1/tools/dmarc-record-checker: parameters, JSON response schema, error codes, and per-IP rate limits. No API key required. ``` GET https://api.domainee.dev/v1/tools/dmarc-record-checker?domain=domainee.dev ``` Look up the DMARC record at _dmarc., parse the policy (`p`), subdomain policy (`sp`), alignment, reporting addresses (`rua`/`ruf`), and percentage. Flags issues like missing policy or too-permissive settings. Free. No API key required. CORS-enabled. Rate-limited per IP at 30/min and 500/day. Parameters: | Name | In | Type | Required | Description | Example | |---|---|---|---|---|---| | `domain` | query | string | yes | Domain to check DMARC for. | `domainee.dev` | Example request: ```bash curl -s "https://api.domainee.dev/v1/tools/dmarc-record-checker?domain=domainee.dev" | jq ``` Example response: ```json { "ok": true, "data": { "domain": "domainee.dev", "record": "v=DMARC1; p=none;", "tags": { "v": "DMARC1", "p": "none" }, "issues": [ { "level": "warning", "message": "Policy is `none` — no enforcement." } ] } } ``` #### See also - [DMARC Record Checker visual tool](https://domainee.dev/free-tools/dmarc-record-checker) — UI version that hits this same API. - [Free APIs overview](https://domainee.dev/docs/free-apis) — rate limits, response envelope, error codes. ### TXT Record Lookup API Reference Source: https://domainee.dev/docs/free-apis/txt-record-lookup Endpoint reference for GET /v1/tools/txt-record-lookup: parameters, JSON response schema, error codes, and per-IP rate limits. No API key required. ``` GET https://api.domainee.dev/v1/tools/txt-record-lookup?domain=domainee.dev ``` Fetch all TXT records on a domain and classify them: SPF, DKIM, DMARC, BIMI, verification tokens (Google, Microsoft, Atlassian, etc.), or general. Free. No API key required. CORS-enabled. Rate-limited per IP at 30/min and 500/day. Parameters: | Name | In | Type | Required | Description | Example | |---|---|---|---|---|---| | `domain` | query | string | yes | Domain to fetch TXT records for. | `domainee.dev` | Example request: ```bash curl -s "https://api.domainee.dev/v1/tools/txt-record-lookup?domain=domainee.dev" | jq ``` Example response: ```json { "ok": true, "data": { "domain": "domainee.dev", "count": 3, "records": [ { "value": "v=spf1 include:_spf.google.com ~all", "classification": "SPF" }, { "value": "google-site-verification=...", "classification": "verification" }, { "value": "MS=ms123456", "classification": "verification" } ] } } ``` #### See also - [TXT Record Lookup visual tool](https://domainee.dev/free-tools/txt-record-lookup) — UI version that hits this same API. - [Free APIs overview](https://domainee.dev/docs/free-apis) — rate limits, response envelope, error codes. ### Website Status Checker API Reference Source: https://domainee.dev/docs/free-apis/website-status-checker Endpoint reference for GET /v1/tools/website-status-checker: parameters, JSON response schema, error codes, and per-IP rate limits. No API key required. ``` GET https://api.domainee.dev/v1/tools/website-status-checker?url=https%3A%2F%2Fdomainee.dev ``` Issue a HEAD/GET to a URL and report whether it's up, the HTTP status, response time in ms, final URL after redirects, and basic SSL info. Free. No API key required. CORS-enabled. Rate-limited per IP at 30/min and 500/day. Parameters: | Name | In | Type | Required | Description | Example | |---|---|---|---|---|---| | `url` | query | string | yes | URL to check (include scheme). | `https://domainee.dev` | Example request: ```bash curl -s "https://api.domainee.dev/v1/tools/website-status-checker?url=https://domainee.dev" | jq ``` Example response: ```json { "ok": true, "data": { "url": "https://domainee.dev", "up": true, "status": 200, "statusText": "OK", "responseTimeMs": 84, "finalUrl": "https://domainee.dev/" } } ``` #### See also - [Website Status Checker visual tool](https://domainee.dev/free-tools/website-status-checker) — UI version that hits this same API. - [Free APIs overview](https://domainee.dev/docs/free-apis) — rate limits, response envelope, error codes. ### DKIM Record Checker API Reference Source: https://domainee.dev/docs/free-apis/dkim-record-checker Endpoint reference for GET /v1/tools/dkim-record-checker: parameters, JSON response schema, error codes, and per-IP rate limits. No API key required. ``` GET https://api.domainee.dev/v1/tools/dkim-record-checker?domain=domainee.dev&selector=google ``` Look up the DKIM record at ._domainkey., parse the public key, key type (rsa/ed25519), and any issues. Validates against common provider conventions (Google `google`, Resend, Mailgun `mxvault`, etc.). Free. No API key required. CORS-enabled. Rate-limited per IP at 30/min and 500/day. Parameters: | Name | In | Type | Required | Description | Example | |---|---|---|---|---|---| | `domain` | query | string | yes | Domain whose DKIM you want to check. | `domainee.dev` | | `selector` | query | string | yes | DKIM selector to look up. Common values: `google`, `k1`, `mxvault`, `selector1`, `s1`. Must be `[a-z0-9_-]+`. | `google` | Example request: ```bash curl -s "https://api.domainee.dev/v1/tools/dkim-record-checker?domain=domainee.dev&selector=google" | jq ``` Example response: ```json { "ok": true, "data": { "domain": "domainee.dev", "selector": "google", "record": "v=DKIM1;k=rsa;p=MIIBI...AB", "keyType": "rsa", "keyBits": 2048, "issues": [] } } ``` #### See also - [DKIM Record Checker visual tool](https://domainee.dev/free-tools/dkim-record-checker) — UI version that hits this same API. - [Free APIs overview](https://domainee.dev/docs/free-apis) — rate limits, response envelope, error codes. ### Domain Availability Checker API Reference Source: https://domainee.dev/docs/free-apis/domain-availability-checker Endpoint reference for GET /v1/tools/domain-availability-checker: parameters, JSON response schema, error codes, and per-IP rate limits. No API key required. ``` GET https://api.domainee.dev/v1/tools/domain-availability-checker?name=mybrand&tlds=com ``` Check availability of a name across one or more TLDs. Uses DNS + RDAP fallback. Returns per-TLD availability with caveats (registrar lock, premium, etc.) where known. Free. No API key required. CORS-enabled. Rate-limited per IP at 30/min and 500/day. Parameters: | Name | In | Type | Required | Description | Example | |---|---|---|---|---|---| | `name` | query | string | yes | The bare name (without TLD), e.g. `mybrand`. | `mybrand` | | `tlds` | query | string | no | Optional repeatable parameter: `?tlds=com&tlds=io&tlds=dev`. Defaults to a standard SaaS set when omitted. | `com` | Example request: ```bash curl -s "https://api.domainee.dev/v1/tools/domain-availability-checker?name=mybrand&tlds=com&tlds=io&tlds=dev" | jq ``` Example response: ```json { "ok": true, "data": { "name": "mybrand", "results": [ { "tld": "com", "available": false }, { "tld": "io", "available": true }, { "tld": "dev", "available": true } ] } } ``` #### See also - [Domain Availability Checker visual tool](https://domainee.dev/free-tools/domain-availability-checker) — UI version that hits this same API. - [Free APIs overview](https://domainee.dev/docs/free-apis) — rate limits, response envelope, error codes. --- ## Not included in this file Blog posts, the 100+ term glossary, competitor comparisons, and use-case pages are listed with one-line summaries in https://domainee.dev/llms.txt. - [Blog](https://domainee.dev/blog) · [Glossary](https://domainee.dev/glossary) · [Alternatives](https://domainee.dev/alternatives) · [Free tools](https://domainee.dev/free-tools) - [Sitemap](https://domainee.dev/sitemap.xml) · [RSS](https://domainee.dev/blog/rss.xml)