Skip to Content

Domains

All endpoints require Authorization: Bearer TOKEN.

Add Domain

POST /api/v1/domains

Body:

{ "domain": "my-app.com", "project_id": "prj_abc123", "site_id": "site_abc123", "is_primary": false }
FieldTypeDescription
domainstringRequired. Hostname to attach
project_idstringRequired. Project the domain belongs to
site_idstring?Site to attach to. Omit to let the platform resolve one — see below
is_primaryboolean?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/:projectId

List Site Domains

GET /api/v1/domains/site/:siteId

The 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:

FieldTypeDescription
redirect_tostring?Present when this hostname redirects instead of serving content. The target is another domain on the same site; the path is preserved
redirect_permanentbooleanfalse → temporary redirect, true → permanent
grace_expires_atstring?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. 302 for browsers, 307 for other methods — so a POST to an old webhook URL still arrives as a POST. 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.app hosts; 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/recheck

Re-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
ParameterTypeDescription
domainstringRequired. 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:

TypeNameValue
A@ or subdomain203.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.