DNS API
Connect a DNS provider, preview each DNS change, and apply a domain's records without copying them by hand.
Adding a custom domain normally ends with Openship telling you which records to create and waiting for you to create them. Connect a DNS provider and Openship can preview and apply those records for you without leaving the page. Applying DNS is always an explicit action: creating a project domain or saving a service route never changes the provider's zone silently. In the dashboard, credentials live under Settings → DNS and each pending domain's records panel contains the apply action.
Cloudflare is the only provider today. The provider list is served by the API rather than hard-coded in the
dashboard, so GET /api/dns/providers is the authoritative answer to "what can I connect?".
Base path & auth
All paths are relative to your instance, under /api — e.g. https://your-host/api/dns/credentials.
Send a personal access token as a bearer header (Authorization: Bearer <token>), created with
openship token create. The dashboard uses your session cookie instead. See the
API overview for the full auth model.
Endpoints
| Method & path | Permission | What it does |
|---|---|---|
GET /api/dns/providers | settings:read | List supported providers and the token scopes each needs. |
GET /api/dns/credentials | settings:read | List connected credentials for the organization. |
GET /api/dns/credentials/:id | settings:read | Read one connected credential. |
POST /api/dns/credentials | settings:admin | Connect a credential. The token is verified before it is stored. |
DELETE /api/dns/credentials/:id | settings:admin | Disconnect a credential. Records already written are left alone. |
POST /api/dns/verify-zone | settings:read | Check whether a connected provider manages a hostname's zone. Creates and changes nothing. |
GET /api/domains/:id/dns/plan | domain:read | Preview the exact record actions for one persisted domain. Creates and changes nothing. |
POST /api/domains/:id/dns/apply | domain:write | Apply that domain's records through the matching connected provider. |
Connect a provider
POST /api/dns/credentialsProp
Type
curl -X POST https://your-host/api/dns/credentials \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"provider":"cloudflare","name":"Cloudflare production","apiToken":"cf-..."}'For Cloudflare, create a token at dash.cloudflare.com/profile/api-tokens scoped to Zone:Zone:Read and Zone:DNS:Edit, limited to the zones you want Openship to manage.
The token is write-only from here on
It is encrypted at rest (AES-256-GCM, key derived from BETTER_AUTH_SECRET) and no endpoint returns it —
not in full and not as a prefix. tokenMasked is a fixed ••••••••. There is no "reveal"; to change a
token, disconnect the credential and connect a new one.
Rotating BETTER_AUTH_SECRET makes stored tokens undecryptable. The credential then reports
status: "invalid" the next time Openship tries to use it, and you re-connect it.
Check a zone
POST /api/dns/verify-zoneProp
Type
The response separates four outcomes, because they call for different actions:
status | matched | Meaning |
|---|---|---|
matched | true | A connected provider hosts this zone; the domain's records can be previewed and applied from Openship. |
none | false | We asked, and no connected provider hosts it. You add the records yourself. |
unauthorized | false | A stored token was rejected. Re-connect it — credentialId says which. |
unavailable | false | The provider could not be reached (rate limit, outage). Unknown, not a sign of misconfiguration. |
curl -X POST https://your-host/api/dns/verify-zone \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"hostname":"app.example.com"}'Add, preview, and apply a domain
POST /api/domains persists a pending domain and returns the records it needs. It does not call the DNS
provider. Service-level custom routes follow the same contract: saving the route creates its pending domain
row, and that row supplies the :id used by the shared plan and apply endpoints.
Preview the exact provider changes before writing anything:
curl https://your-host/api/domains/dom_123/dns/plan \
-H "Authorization: Bearer $OPENSHIP_TOKEN"{
"data": {
"status": "matched",
"provider": "cloudflare",
"zoneName": "example.com",
"records": [
{ "name": "app.example.com", "type": "A", "action": "create", "desired": "203.0.113.10" }
]
}
}The plan classifies every record as create, update, adopt, in-sync, or conflict. A conflict is
reported rather than overwritten. status: "none" means no connected credential manages the zone, so use
the records returned by POST /api/domains (or GET /api/domains/:id/records) and add them manually.
Apply the previewed records explicitly:
curl -X POST https://your-host/api/domains/dom_123/dns/apply \
-H "Authorization: Bearer $OPENSHIP_TOKEN"The response reports applied, skipped, or failed for every record. provisioned is true only when
the matching provider has every actionable record in place; a partial write returns provisioned: false
with a reason and the per-record failures.
No verification is triggered by apply. The records are seconds old, a resolver may still be holding a
negative answer for the name, and a failed check burns one of Let's Encrypt's per-hostname validation
failures. The domain stays pending and the domains:verify-pending sweep picks it up after its
ten-minute grace window.
Removing records
Disconnecting a credential does not touch DNS. Removing a domain does — but only records Openship created,
identified by the Managed by Openship comment it writes on every record.
Why ownership is tracked, not inferred
For an apex domain the record name is the zone apex, where your MX, SPF TXT and CAA records also
live. Deleting "every record at this name" would take your mail with it, so Openship deletes only what it
can prove it wrote.
Connecting a domain does repoint an existing record at that name — that is what connecting it means — but
repointing is not adoption: Openship leaves such a record's comment untouched, so it never carries the
Managed by Openship marker and removing the domain later will not delete it.
Errors
| Code | Status | Meaning |
|---|---|---|
DNS_UNKNOWN_PROVIDER | 400 | provider is not a name from GET /api/dns/providers. |
DNS_PROVIDER_NOT_READY | 400 | The provider rejected the token during the pre-store check. |
DNS_RECORD_CONFLICT | 409 | Several records already answer for that name and type and none are Openship's — a round-robin or multi-host set it will not rewrite. |
DNS_API_ERROR | 502 | The provider's API failed. The upstream status is in the message. |