# @dodomain/node (https://dodomain.io/docs/node-sdk)

The TypeScript server SDK: sessions, connections, apps, domains, webhook endpoints, key rotation, and the webhook verifier.

```bash
npm install @dodomain/node
```

A typed client over the whole `/api/v1` surface, plus the webhook verifier, in one package with **zero runtime dependencies**. It ships dual ESM + CJS builds and self-contained type declarations, and requires Node 20+.

```ts
import { DoDomain, verifyWebhook } from "@dodomain/node";

const dodomain = new DoDomain({ secretKey: process.env.DODOMAIN_SECRET_KEY });

const session = await dodomain.sessions.create({
  domain: "customer.com",
  records: [{ type: "CNAME", host: "app", value: "cname.yourproduct.com" }],
  returnUrl: "https://yourproduct.com/settings/domains",
});
// session.connectUrl → hand to your user
```

Keep the secret key server-side only; it must never reach a browser.

## The client [#the-client]

`new DoDomain(options)` takes:

| Option      | Type                 | Notes                                                                                                                           |
| ----------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `secretKey` | string, **required** | Your app's `dd_sk_...` key. A value that doesn't start with `dd_sk_` throws at construction, before any call is made.           |
| `baseUrl`   | string               | API origin. Defaults to `https://app.dodomain.io`, the one origin that actually serves `/api/v1/*` and the hosted connect page. |
| `fetchImpl` | `typeof fetch`       | Bring your own `fetch` (custom agent, instrumentation, a test double).                                                          |

## Errors [#errors]

Every failure this SDK produces is a `DoDomainError`: never a raw `SyntaxError` from a proxy's HTML error page, never an unchecked cast.

```ts
import { DoDomainError } from "@dodomain/node";

try {
  await dodomain.connections.reverify(connectionId);
} catch (err) {
  if (err instanceof DoDomainError) {
    console.error(err.message, err.status, err.body);
  }
}
```

- `message` is the API's `error` code (`not_found`, `quota_exceeded`, `rate_limited`, …), or `non_json_response` / `invalid_response_shape` when the body wasn't what the contract promises.
- `status` is the HTTP status, **or `0`, meaning no request was sent**: the SDK validates your arguments against the same schemas the server enforces, so a malformed call fails locally instead of round-tripping for the identical 400.
- `body` is the decoded response body (or the raw text for a non-JSON one).

Responses are validated on the way back too, against the same schemas the routes are typed against; a body that doesn't match raises `invalid_response_shape` rather than handing you a mistyped object.

## sessions [#sessions]

### `sessions.create(input)` [#sessionscreateinput]

Mints a connect session. `records[].host` is relative to `domain` (`"app"` under `customer.com` composes to `app.customer.com`; `"@"` means the domain itself), and MX records need a `priority`.

The returned `records` are the **composed** names: each requested record paired with the `fqdn` verification will look up. A `warnings` array appears only when there is something worth flagging about an accepted request; branch on `warning.code`, never on the message text. See [Creating a session](https://dodomain.io/docs/api#creating-a-session) for the full contract.

### `sessions.get(sessionId)` [#sessionsgetsessionid]

Reads a session back **by id** with your credential: the id every webhook payload carries. This is how a `session.abandoned` or `session.completed` receiver asks what state a session actually ended in.

- Its `records` are the **composed** names: `type`, `host` and `fqdn`, with **no `value`**. The record targets are what you sent; this arm reports the names DoDomain looks up.
- It reads an **expired** session rather than refusing it. Check the returned `expired` flag, which is derived at read time (`expiresAt <= now`); never infer expiry from `status` alone.
- Passing a `dd_sess_...` token throws a `DoDomainError` with `status: 0`. That token addresses the same path's public arm, which answers with a different shape, so the SDK names the mistake instead of failing opaquely later.
- Unknown and not-yours both throw `status: 404`, deliberately indistinguishable.

## connections [#connections]

| Method                               | Does                                                                                                                                    |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `connections.list(filters?)`         | One page of connections, newest first. Filters: `appId`, `domain`, `limit` (1–100, server default 50), `cursor`, `includeDisconnected`. |
| `connections.get(connectionId)`      | One connection, disconnected ones included. Same shape as a `list` element; unknown or not-yours throws `404`.                          |
| `connections.delete(connectionId)`   | Disconnect: archives it and stops DNS monitoring. The customer's DNS records stay in place.                                             |
| `connections.reverify(connectionId)` | Queue an on-demand DNS recheck.                                                                                                         |

Only the filters you actually pass go on the wire, so an omitted one always means _the server's current default_ rather than a default frozen into the SDK build you installed. Follow `nextCursor` to walk past a page; it is `null` on the last one.

`delete` is idempotent: a repeat returns the **original** `disconnectedAt` with `alreadyDisconnected: true` and emits no second webhook.

`reverify` returns as soon as the job is accepted (HTTP 202). The verdict arrives as a `connection.verified` / `connection.failed` webhook, never in the response. A connection that was already checked within the last 10 minutes throws `status: 429`.

## apps and domains [#apps-and-domains]

`apps.list()` returns the apps this credential can see. A secret key sees exactly its **own** app; listing siblings would widen one leaked key into team-wide reconnaissance. Only the publishable `dd_pk_...` key is ever returned; no secret material is in the contract.

`domains.check({ domain })` is a stateless pre-flight: which provider hosts the domain, which zone owns its records, which connect tier and method it will get, its nameservers, whether Domain Connect was discovered, whether one-click is actually available there (`oneClickAvailable`, 0.8.0+; `tier: 2` alone only means the provider speaks Domain Connect, not that it serves our templates), and the manual guide if it comes to that. Nothing is persisted and no session is created, so it is the call to make **before** deciding what UI to show. Available on every plan.

## webhookEndpoints [#webhookendpoints]

The same delivery targets the dashboard's **Webhook endpoints** card manages, so they can live in your CI or infrastructure-as-code instead of in someone's browser history.

| Method                                 | Does                                                                   |
| -------------------------------------- | ---------------------------------------------------------------------- |
| `webhookEndpoints.list()`              | Every endpoint this app delivers to. **Never** returns `secret`.       |
| `webhookEndpoints.create({ url })`     | Register a target. Returns the endpoint **plus its signing secret**.   |
| `webhookEndpoints.update(id, { url })` | Repoint an endpoint. The signing secret is deliberately untouched.     |
| `webhookEndpoints.delete(id)`          | Stop delivering to it.                                                 |
| `webhookEndpoints.rotateSecret(id)`    | Mint a new signing secret and return it.                               |
| `webhookEndpoints.resume(id)`          | Resume an [auto-paused](https://dodomain.io/docs/webhooks#paused-endpoints) endpoint (0.7.0+). |

<Callout type="warn" title="secret is shown once">
  `secret` appears in the results of `create` and `rotateSecret` and in no read surface ever again;
  a leaked read must not be able to recover the ability to forge our signatures. Persist it in the
  same code path that received it. It is the `secret` argument to `verifyWebhook`.
</Callout>

A non-`https` url, one resolving to a localhost/private/link-local address, and a duplicate of a url this app already registered all throw `status: 400` with the reason in `body.message`. At the plan's endpoint cap it is `status: 402`. An unknown id and another app's id both throw `status: 404`.

Rotation is **immediate and total**: the worker reads the secret live at delivery time, so signatures switch at once, including retries of deliveries queued before you rotated. Deploy the new secret to your receiver promptly.

## keys [#keys]

`keys.rotate()` rotates the calling app's own secret key, which is what makes scheduled credential rotation automatable instead of a dashboard click.

<Callout type="warn" title="No grace window">
  The key that authorized the call stops authenticating the instant it returns, so the returned
  `secretKey` is the **only** copy of the new credential: persist it before doing anything else. The
  client instance you called it on keeps using the old key; construct a new `DoDomain` with the
  returned `secretKey` to keep working. `publicKey` is echoed unchanged, so an automated job can
  assert it rewrote the app it meant to.
</Callout>

There is deliberately no create/list/revoke-another-key on this API: a stolen `dd_sk_` must not be able to mint a second, hidden credential that survives the owner rotating the one they know about. See [Rotating your secret key](https://dodomain.io/docs/api#rotating-your-secret-key) for a scheduled-rotation script.

## verifyWebhook [#verifywebhook]

```ts
import { verifyWebhook } from "@dodomain/node";

// signature header: x-dodomain-signature, "t=<unix ms>,v1=<hex>"
const ok = verifyWebhook(secret, rawBody, signatureHeader);
```

`verifyWebhook(secret, body, header, toleranceMs?, nowMs?)` returns a boolean. Pass the **raw** body: the signature covers the exact bytes sent, so a re-serialized JSON object will not verify. `toleranceMs` is the replay window (five minutes by default) and `nowMs` exists so you can verify an archived delivery in a test. The function is exported from this package precisely so an integrator needs only this one dependency to both mint sessions and verify what comes back.

## Types [#types]

Every shape the client returns is exported as a type, and each one is pinned at compile time to the zod schema the API validates with, so a server-side schema change that drifted from these declarations would fail the SDK's own build rather than reach you as a runtime surprise.

`ConnectSessionSummary`, `IntegratorSession`, `ComposedDnsRecord`, `DnsRecord`, `DnsRecordType`, `SessionWarning`, `Connection`, `ConnectionStatus`, `ListConnectionsInput`, `ListConnectionsResult`, `DisconnectConnectionResult`, `ReverifyConnectionResult`, `App`, `ListAppsResult`, `CheckDomainInput`, `CheckDomainResult`, `DetectionTier`, `DetectionMethod`, `DetectionConfidence`, `ProviderGuide`, `WebhookEndpoint`, `WebhookEndpointInput`, `WebhookEndpointWithSecret`, `ListWebhookEndpointsResult`, `DeleteWebhookEndpointResult`, `RotateSecretKeyResult`, `WebhookEvent`, `WebhookEventType`, `WebhookEventWire`.

Type your webhook receiver against `WebhookEvent`: `{ id, type, occurredAt, data }`. `WebhookEventWire` is the same envelope plus the deprecated `event` alias that is still on the wire for receivers written before 2026-08-06; reach for it only if you still read that field. `ConnectSessionSummary` is what `sessions.create` returns; it was called `Session` before 0.5.0, and that name remains exported as a `@deprecated` alias until the next major; switch your imports now. `CreateSessionResponse` and `SessionRecord` likewise remain exported as aliases of `ConnectSessionSummary` and `DnsRecord` for code written against the earliest releases.
