Domains
All endpoints require Authorization: Bearer TOKEN.
Add Domain
POST /api/v1/domainsBody:
{
"domain": "my-app.com",
"project_id": "prj_abc123",
"site_id": "site_abc123",
"is_primary": false
}| Field | Type | Description |
|---|---|---|
domain | string | Required. Hostname to attach |
project_id | string | Required. Project the domain belongs to |
site_id | string? | Site to attach to. Omit to let the platform resolve one — see below |
is_primary | boolean? | Mark as the site’s primary domain |
Response: 201
{
"id": "dom_abc123",
"project_id": "prj_abc123",
"site_id": "site_abc123",
"domain": "my-app.com",
"is_primary": false,
"ssl_enabled": true,
"redirect_permanent": false,
"pending_dns": true,
"created_at": "2026-09-24T10:00:00Z"
}pending_dns: true means the domain is attached but not published yet because
its DNS does not point at Ghayma. It clears on its own once DNS resolves, or on
demand with Recheck DNS.
Which site an omitted site_id resolves to
With site_id omitted, the platform walks the shared resolution
ladder: the project’s default site →
the site named main → the only site.
If the project has several sites and none is marked, the request is refused with
409 rather than guessed at — pointing a customer’s hostname at the wrong app
is not a recoverable mistake. Name the site, or set a
default.
If the project has no sites at all, attaching a custom domain counts as a
first app action: the main site and its default .ghayma.app URL are created,
and your domain attaches to it.
A collaborator restricted to some
sites can add a domain only to a
site they can write: the one site_id names or, when it is omitted, the one the
ladder picks. Otherwise the request answers 403.
List Project Domains
Domains are always listed by scope — there is no “list all domains” route.
GET /api/v1/domains/project/:projectIdList Site Domains
GET /api/v1/domains/site/:siteIdThe domain object
Beyond id, domain, project_id, site_id, is_primary, ssl_enabled,
pending_dns (attached but waiting for DNS) and created_at, a domain row can
carry redirect state:
| Field | Type | Description |
|---|---|---|
redirect_to | string? | Present when this hostname redirects instead of serving content. The target is another domain on the same site; the path is preserved |
redirect_permanent | boolean | false → temporary redirect, true → permanent |
grace_expires_at | string? | Present only on a redirect left behind by a rename. The row is removed after this instant |
site_id, redirect_to and grace_expires_at appear only when set.
Grace redirects
Renaming a site — or renaming its project — moves the site’s .ghayma.app URL.
The old hostname is not deleted at the cutover: it is kept as a temporary
redirect to the new URL for 30 days, so links, bookmarks, webhook
registrations and CI scripts already in the wild keep landing on the app. A daily
sweep removes the row once grace_expires_at has passed, and the old hostname
stops resolving.
A grace redirect is an ordinary domain row — it appears in List Site
Domains with redirect_to set to the new host,
redirect_permanent: false, and grace_expires_at 30 days out:
{
"id": "dom_ghi789",
"domain": "my-app.ghayma.app",
"project_id": "prj_abc123",
"site_id": "site_abc123",
"redirect_to": "my-app-public-api.ghayma.app",
"redirect_permanent": false,
"grace_expires_at": "2026-09-07T19:00:00Z"
}- Temporary on purpose.
302for browsers,307for other methods — so aPOSTto an old webhook URL still arrives as aPOST. A permanent redirect would be cached by browsers long after the window closed. - One per site. A second rename drops the previous grace row, so repeated renames never chain.
- Reclaimable. A grace redirect of the same project never blocks a rename or a site create that wants its hostname back — it is taken over. Renaming back, or re-creating a site under its old name, restores the URL immediately.
- Deletable early. It is a normal row: Delete Domain ends the window ahead of schedule.
- Custom domains are never touched. Renames only move
.ghayma.apphosts; your own domains stay attached to the site and keep serving throughout.
Set Redirect
PUT /api/v1/domains/:id/redirect{ "target": "www.my-app.com", "permanent": false }Points the domain at another domain on the same site (the path is
preserved). An empty or omitted target clears the redirect. Returns the
updated domain row; an invalid target → 422.
Recheck DNS
POST /api/v1/domains/:id/recheckRe-runs the DNS check for one domain and repairs what it finds: a domain whose
DNS now points here is published, and a published domain that never got a
certificate gets a fresh one ordered. Returns the check result (same shape as
Check Domain DNS) plus pending_dns.
Delete Domain
DELETE /api/v1/domains/:id?project_id=prj_abc123:id is the domain’s id or its hostname (what ghayma domain delete sends), in
any letter case and with or without a trailing dot. project_id is required —
without it the request is refused with 400. A domain the project does not hold
answers 404 domain not found.
A collaborator restricted to some
sites can delete only the
domains of sites they can write (403 otherwise). A restricted
token can delete its projects’ domains by id or
by hostname.
Check Domain DNS
Check whether a domain’s DNS points at Ghayma and whether its certificate has been issued. Useful before adding a domain, and to poll while DNS propagates.
GET /api/v1/domains/check?domain=my-app.com| Parameter | Type | Description |
|---|---|---|
domain | string | Required. Hostname to check. Missing → 400 |
Response: 200
{
"domain": "my-app.com",
"dns": {
"ok": true,
"resolved_ips": ["203.0.113.10"]
},
"ssl": {
"ok": true,
"issuer": "Let's Encrypt",
"expiry": "2026-10-29T12:00:00Z"
}
}203.0.113.10 above is an example — the real address is the one this endpoint
and the Dashboard report for your domain. dns.ok is true when the domain
resolves to the platform IP directly, via a
CNAME to a *.ghayma.app host, or through Cloudflare with a valid certificate
already served from our edge. ssl.issuer and ssl.expiry are present only
once a certificate has been issued.
DNS Configuration
Point your domain at the Ghayma server. 203.0.113.10 is an example — copy the
real IPv4 address from the Dashboard (Project → Domains → Add domain), which
is also what ghayma domain create prints:
| Type | Name | Value |
|---|---|---|
| A | @ or subdomain | 203.0.113.10 (example) |
SSL certificates are provisioned automatically via Let’s Encrypt once DNS resolves correctly.
One A record, and no AAAA. Ghayma serves IPv4 only: the hostname needs
exactly one A record pointing at the address shown in the Dashboard, and no
AAAA (IPv6) record. Let’s Encrypt prefers IPv6 whenever an AAAA exists, so a
stray AAAA left behind by a previous host answers the challenge instead of us
and the certificate never issues — Check Domain DNS keeps
reporting ssl.ok: false. Delete the AAAA record and check again. A
Cloudflare-proxied (orange-cloud) domain is fine as-is.