Docs
Docs
Getting started

Quickstart

Live domain in five minutes. Six requests, real responses, nothing to install.

Runs nightly against production

Get a key

Sign in, open Settings, create a key. Use a test key first: test domains never contact a certificate authority and walk every state on a ten-second timer.

Request
# Create a test key in Settings, or:
curl -X POST https://api.apexly.dev/v1/keys \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"name":"Staging","mode":"test"}'
Response201 Created
{
  "id": "key_01M551Z48QJAYMT02C93PBX0MF",
  "key": "sk_test_zbjrDw…",   // shown once
  "mode": "test"
}

Tell us where your app lives

Set the origin once per app. Every customer domain forwards here with the original Host header. We fetch it once to confirm it answers.

Request
curl -X PATCH https://api.apexly.dev/v1/apps/current \
  -H "Authorization: Bearer sk_test_…" \
  -H "Content-Type: application/json" \
  -d '{"origin_url":"https://app.hireflow.com"}'
Response200 OK
{
  "id": "app_01M541VYPHCHS1EMA417TKMA3K",
  "origin_url": "https://app.hireflow.com",
  "origin_reachable": true
}

Create a domain

One request per customer hostname. The response carries the single DNS record to hand over and a connect link that walks the customer through it.

Idempotent on hostname: calling it again returns the same domain with a 200.
Request
curl -X POST https://api.apexly.dev/v1/domains \
  -H "Authorization: Bearer sk_test_…" \
  -H "Content-Type: application/json" \
  -d '{"hostname":"careers.acme.com","metadata":{"customer":"acme"}}'
Response201 Created
{
  "id": "dom_01M551Z4AYJ9G7AW89XTQN0DFK",
  "hostname": "careers.acme.com",
  "status": "pending_dns",
  "dns": {
    "cname": {
      "name": "careers",
      "value": "d-tuih5b.cname.apexly.dev"
    }
  },
  "connect_url": "https://connect.apexly.dev/c/kuuII7…"
}

Hand over the record

Send the connect link, or show the record in your own UI. We check every 15 seconds for the first hour, then every five minutes for 72 hours.

Request
# Poll if you like; webhooks are better.
curl https://api.apexly.dev/v1/domains/dom_01M551Z4AYJ9G7AW89XTQN0DFK \
  -H "Authorization: Bearer sk_test_…"
Response200 OK
{
  "id": "dom_01M551Z4AYJ9G7AW89XTQN0DFK",
  "status": "verifying",
  "status_reason": "sandbox",
  "last_checked_at": null,
  "next_check_at": "2026-10-17T13:49:25.646Z"
}

Listen for domain.active

Verification, issuance and activation each fire a signed webhook. Verify the signature, read data.domain.metadata, mark your tenant live.

Request
# Your endpoint receives:
POST /webhooks/apexly
Apexly-Signature: t=1760587971,v1=5f1c…

{
  "type": "domain.active",
  "data": { "domain": { "id": "dom_01M551Z4AYJ9G7AW89XTQN0DFK",
    "hostname": "careers.acme.com",
    "status": "active",
    "metadata": { "customer": "acme" } } }
}
Your reply
# Reply 2xx within 10 s.
# Retries: 1m, 5m, 30m, 2h, 6h, 12h, 24h

Trust the signature at your origin

Read the Host header to find the tenant. Reject any request whose X-Apexly-Signature does not verify; that is the only change your app needs.

Most frameworks block unknown hosts by default. Allow your managed hostnames, or allow by signature.
Request
const sig = req.headers["x-apexly-signature"];
const ts  = req.headers["x-apexly-timestamp"];
const ok  = verify(secret, `${ts}.${req.headers.host}`, sig);
if (!ok) return res.status(401).end();
const tenant = await tenantByHost(req.headers.host);
Headers we add
# Headers we add on every request:
Host: careers.acme.com
X-Forwarded-Proto: https
X-Apexly-Domain-Id: dom_01M551Z4AYJ9G7AW89XTQN0DFK
X-Apexly-Timestamp: 1760587971
X-Apexly-Signature: 9a3f…

What to read next

Was this helpful?
Edit this page