# Begin the sign-in to the DNS provider for a connection

> Begin the sign-in to the DNS provider for a connection: CustomDomain™ API reference in the connections group. Covers parameters, request body and responses.

Source: https://docs.customdomain.ai/docs/api-reference/connections/startConnectionOAuth

## `POST https://api.customdomain.ai/v1/connections/{id}/oauth:start`

**Rail C (flagship).** Returns the URL the widget opens in a popup, bound to an HMAC-signed single-use `state`. The provider redirects back to `/v1/connect/oauth/callback`, which writes the connection's records with what the provider handed over. `flow` says which sign-in runs: `oauth2` (authorization code with PKCE S256; for Google Cloud DNS, Sign in with Google Cloud, whose callback first finds the Google Cloud project that holds the domain's public zone; for Azure DNS, a Microsoft sign-in whose callback first finds the Azure subscription and resource group that hold it), `vercel_integration` (the CustomDomain™ integration's install page; the user picks the Vercel team that owns the domain), `porkbun_handoff` (Porkbun's PKCE API key handoff: the user approves a key named after the domain; no client registration is needed), `porkbun_dnsconnect` (Porkbun's DNS Connect, run in place of the handoff for a connect when the deployment holds an application Porkbun approved, `OAUTH_PORKBUN_CLIENT_ID` and `OAUTH_PORKBUN_CLIENT_SECRET`: the domain's records are signed into the request, the user approves them at Porkbun, and Porkbun adds them itself, with no API access switch needed; nothing comes back, so `managed` is always false, and signing in to remove the domain still runs the handoff), `ovh_consent` (an OVHcloud consumer key limited to the domain's DNS zone, validated on OVHcloud's page for the chosen `region`) or `mcp_oauth` (OAuth through the provider's hosted MCP server, for Hostinger, Unstoppable Domains, XServer, Namecheap, Spaceship and Dynadot: the endpoints are discovered from the server's metadata, the client is one the control plane registered for itself, and the records are written through the server's DNS tools; offered only for the providers the deployment lists in `MCP_SIGNIN_PROVIDERS`). The credential is used for that write and dropped, unless the deployment keeps grants for this provider (`OAUTH_MANAGED_PROVIDERS` with `GRANT_ENCRYPTION_KEY`), in which case `managed` is true and it is stored encrypted for `:reapply`.

A widget JWT starts the sign-in for its own application's connections (a domain-bound token for its own domain only); an API key for a connection of any application in its tenant, and a member's key needs a role that operates apps.

A widget token must be the connection's end user: a connection that names another end user than the token's `end_user_ref`, or names one while the token names none, answers `403` `token_wrong_end_user` before anything is read or changed, and a connection that names none is any token's. An API key, an app-scoped token and a delegated agent token are not limited by it.

Signing in to remove the domain (`purpose: remove`) keeps its own answer, `403` `forbidden`, for a token that is not the end user's, and for a share link's token on every connection: a link sets a domain up and never removes it.

### Authentication

`widgetJWT`, `apiKey`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body (JSON)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `provider` | string | no | DNS provider id; defaults to the detected provider. |
| `return_origin` | string | yes | Web origin the callback page may postMessage the outcome to; must be on the server's allowlist. |
| `region` | `eu`, `ca`, `us` | no | OVHcloud only: the API region the user's OVHcloud account lives in (default `eu`). The domain check's `rails.oauth.regions` lists the regions this deployment offers. |
| `purpose` | `connect`, `remove` | no | `remove` signs in once more to remove the records we added (and put back what they replaced) for a connection whose access we did not keep, then disconnects the domain, as `GET /connections/{id}/removal` offers when its `record_removal` is `reauthorize`. The sign-in is used for the removal and withdrawn; it is never kept. The callback's message carries `purpose: "remove"` and the `records` outcome, and `GET /connections/{id}/removal` answers it once the domain is gone. An end user's widget token must be bound as for `DELETE /connections/{id}`. |
| `access` | `continuous`, `one_time` | no | The person's choice for a connect. `continuous` keeps the sign-in, encrypted, so we can put back records that disappear and remove them when the domain is removed; offered only where the deployment can keep this provider's sign-in (`rails.oauth.managed` on the domain check). `one_time` uses it for this write and drops it. Omitted, the deployment's default applies (kept where it can be). |
| `new_sign_in` | boolean | no | "Use another account." A connect sign-in for a domain that a saved key may already act for (`GET /connections/{id}/saved-access`) is refused with `409` `saved_access_available`, before anything is started; `true` signs in anyway. Never needed by an end user's widget token, which is never refused (it may be offered its own key by the domain check, but that is not a refusal yet). |

### Responses

| Status | Description |
| --- | --- |
| `200` | The URL to open, and what it runs |
| `400` | Error envelope: {code, title, details}. |
| `401` | Error envelope: {code, title, details}. |
| `403` | `forbidden`: the credential may not do this. `workspace_suspended`: see the `ForbiddenOrSuspended` response. `token_wrong_end_user`: a widget token that is not this connection's end user (the connection names another end user than the token's `end_user_ref`, or names one while the token names none); nothing is read, written or changed, and the answer shows nothing of the connection. Mint the token with the end user's `end_user_ref`. |
| `404` | Error envelope: {code, title, details}. |
| `409` | `saved_access_available`: a connect sign-in for a domain that a saved key may already act for. Use it (`POST /connections/{id}:use-saved-access`), or send `new_sign_in: true` to sign in with another account. |
| `429` | `provider_rate_limited`: the provider refused to start another sign-in from our servers for now (Porkbun allows 20 key requests an hour per IP address); `details` says when to retry. |
| `502` | `provider_unavailable`: the provider did not start the sign-in (Porkbun key request, OVHcloud consumer key request). |
| `503` | Error envelope: {code, title, details}. |

### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `authorize_url` | string | yes |  |
| `provider` | string | yes |  |
| `flow` | `oauth2`, `vercel_integration`, `porkbun_handoff`, `porkbun_dnsconnect`, `ovh_consent`, `mcp_oauth` | yes |  |
| `managed` | boolean | no | The connect will keep its credential (managed mode). |
| `access` | `continuous`, `one_time` | no | What will happen to the credential after the sign-in. |
| `purpose` | `remove` | no | Present for a removal sign-in. |
| `callback_origin` | string | no | Origin the result will be postMessaged from; check `MessageEvent.origin` against it. |
| `region` | string | no | OVHcloud only, the region the consent was requested in. |

The full specification is served at `GET https://api.customdomain.ai/v1/openapi.json`.
