# Quickstart

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

## 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.

```bash tab="curl"
# 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"}'
```

```js tab="Node"
// Create a test key in Settings, or:
const res = await fetch("https://api.apexly.dev/v1/keys", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_live_…",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "Staging", mode: "test" }),
});
const { key } = await res.json(); // shown once
```

```python tab="Python"
# Create a test key in Settings, or:
import requests

res = requests.post(
    "https://api.apexly.dev/v1/keys",
    headers={"Authorization": "Bearer sk_live_…"},
    json={"name": "Staging", "mode": "test"},
)
key = res.json()["key"]  # shown once
```

```json
{
  "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.

```bash tab="curl"
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"}'
```

```js tab="Node"
const res = await fetch("https://api.apexly.dev/v1/apps/current", {
  method: "PATCH",
  headers: {
    Authorization: "Bearer sk_test_…",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ origin_url: "https://app.hireflow.com" }),
});
const app = await res.json();
```

```python tab="Python"
import requests

res = requests.patch(
    "https://api.apexly.dev/v1/apps/current",
    headers={"Authorization": "Bearer sk_test_…"},
    json={"origin_url": "https://app.hireflow.com"},
)
app = res.json()
```

```json
{
  "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.

```bash tab="curl"
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"}}'
```

```js tab="Node"
const res = await fetch("https://api.apexly.dev/v1/domains", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_…",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    hostname: "careers.acme.com",
    metadata: { customer: "acme" },
  }),
});
const domain = await res.json();
```

```python tab="Python"
import requests

res = requests.post(
    "https://api.apexly.dev/v1/domains",
    headers={"Authorization": "Bearer sk_test_…"},
    json={
        "hostname": "careers.acme.com",
        "metadata": {"customer": "acme"},
    },
)
domain = res.json()
```

```json
{
  "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.

```bash tab="curl"
# Poll if you like; webhooks are better.
curl https://api.apexly.dev/v1/domains/dom_01M551Z4AYJ9G7AW89XTQN0DFK \
  -H "Authorization: Bearer sk_test_…"
```

```js tab="Node"
// Poll if you like; webhooks are better.
const res = await fetch("https://api.apexly.dev/v1/domains/dom_01M551Z4AYJ9G7AW89XTQN0DFK", {
  headers: { Authorization: "Bearer sk_test_…" },
});
const domain = await res.json();
```

```python tab="Python"
# Poll if you like; webhooks are better.
import requests

res = requests.get(
    "https://api.apexly.dev/v1/domains/dom_01M551Z4AYJ9G7AW89XTQN0DFK",
    headers={"Authorization": "Bearer sk_test_…"},
)
domain = res.json()
```

```json
{
  "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.

```http tab="HTTP"
# 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" } } }
}
```

```js tab="Node"
// Express: verify, then mark the tenant live.
app.post("/webhooks/apexly", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyWebhook(secret, req.headers["apexly-signature"], req.body)) {
    return res.status(400).end();
  }
  const event = JSON.parse(req.body);
  if (event.type === "domain.active") markLive(event.data.domain.metadata.customer);
  res.status(204).end();
});
```

```python tab="Python"
# Flask: verify, then mark the tenant live.
@app.post("/webhooks/apexly")
def apexly_webhook():
    if not verify_webhook(secret, request.headers["Apexly-Signature"], request.get_data()):
        abort(400)
    event = request.get_json()
    if event["type"] == "domain.active":
        mark_live(event["data"]["domain"]["metadata"]["customer"])
    return "", 204
```

```text
# 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.

```js tab="Node"
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);
```

```python tab="Python"
sig = request.headers["X-Apexly-Signature"]
ts = request.headers["X-Apexly-Timestamp"]
if not verify(secret, f"{ts}.{request.host}", sig):
    abort(401)
tenant = tenant_by_host(request.host)
```

```http
# 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…
```

- [How it works](/docs/concepts/domain-states)
- [Verify the signature at your origin](/docs/guides/nextjs#verify-the-signature)
- [Webhooks](/docs/webhooks)
- [Next.js guide](/docs/guides/nextjs)
