DNS API
Connect a DNS provider token so Openship writes your domains' records itself, instead of printing them for you to paste.
Adding a custom domain normally ends with Openship telling you which records to create and waiting for you to go and create them. Connect a DNS provider here and it writes them itself: the domain verifies on its own and gets its certificate without you leaving the page. In the dashboard this is Settings → DNS.
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. |
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; records for this domain will be written automatically. |
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"}'What happens when you add a domain
With a provider connected, POST /api/domains writes the records it would otherwise have only shown you,
and the response carries an extra autoDns object reporting what happened per record:
{
"data": { "id": "dom_123", "hostname": "app.example.com" },
"records": { "mode": "selfhosted", "records": [{ "type": "A", "name": "app.example.com", "value": "203.0.113.10" }] },
"autoDns": {
"provisioned": true,
"records": [{ "name": "app.example.com", "type": "A", "outcome": "applied" }]
}
}autoDns is absent when no connected provider manages the zone — that absence is the signal to show the
records for the operator to add by hand. When it is present, provisioned is true only if every record
landed; a partial write reports provisioned: false with a reason and the per-record outcome, so
"we wrote them" and "we wrote some of them" are never the same answer.
No verification is triggered at that moment. 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. |