propgate docs

SDKOverview

SDK

@propgate/sdk is the API from Node, typed. Every route this reference documents has a method here, except signup — see below.

npm install @propgate/sdk
import { Propgate } from "@propgate/sdk";

// Falls back to PROPGATE_API_KEY when the argument is omitted.
const propgate = new Propgate("pg_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");

const { data, error } = await propgate.domains.check("019fcf7a-2b3c-7d4e-9f5a-6b7c8d9e0f1a");

if (error) {
  console.error(error.code, error.message);
} else {
  console.log(data.state, `${data.requirementsMet}/${data.requirementsTotal}`);
}

Published as @propgate/sdk, MIT licensed, and requires Node 20 or later. Its only dependency is @propgate/dns, and only for types: the diagnosis taxonomy is a public contract, so the codes you switch on are the same union the evaluators produce rather than a copy that drifts. Nothing else is installed — requests go over the global fetch.

Nothing throws

Every method returns { data, error, meta }, the same envelope the API puts on the wire. A failed call is a value, not an exception.

const { data, error, meta } = await propgate.domains.list({ state: "failed" });

if (error !== null) {
  // error.code is a union: "not_found" | "rate_limited" | "unauthorized" | …
  // error.statusCode is the HTTP status, or 0 when there never was a response.
  return;
}

// data is a Domain[] from here on, with no cast and no non-null assertion.
for (const domain of data) {
  console.log(domain.name, domain.state);
}

meta.nextCursor; // null when there is no further page

The reason is narrower than a style preference: a catch binds unknown, so the compiler cannot tell you that you forgot to handle a 409. Narrowing on error makes the failure path type-checked, and makes data non-null for the rest of the function without a cast.

PropgateError is still an Error subclass. If your codebase prefers exceptions, throw result.error keeps a stack and instanceof PropgateError works wherever it lands. See Errors and retries for what the error carries.

meta stays beside the data rather than being folded into it, because that is where the answers live that are about the call rather than in it: nextCursor on a page, created on an idempotent register, resolver on a check, previousSecretExpiresAt on a rotation. Each method types its own.

Configuration

const propgate = new Propgate(process.env.PROPGATE_API_KEY, {
  baseUrl: "https://api.propgate.dev",
  maxRetries: 2,
  timeoutMs: 30_000,
  fetch: myInstrumentedFetch,
});

The key falls back to PROPGATE_API_KEY. A missing key is not an error at construction — checks.run and health do not need one — but every other call fails immediately with code: "missing_api_key", naming the two ways to supply one, rather than spending a round trip to be told 401.

fetch is injectable for a proxy agent, a custom TLS setup, or to record what the client sends. baseUrl is what points a test suite at a local stack.

Both timeoutMs and an AbortSignal are also per call, and per call wins:

const controller = new AbortController();

setTimeout(() => controller.abort(), 5000);

const { error } = await propgate.domains.listAll(
  { state: "failed" },
  { signal: controller.signal, timeoutMs: 60_000 }
);

error?.code; // "aborted" if the controller fired first

The public checker needs no key

import { Propgate } from "@propgate/sdk";

// No key: checks.run and health are the two calls that do not need one.
const propgate = new Propgate();

const { data } = await propgate.checks.run({
  domain: "example.com",
  checks: ["spf", "dkim"],
  dkimSelectors: ["google"],
});

for (const finding of data?.findings ?? []) {
  console.log(finding.severity, finding.code, finding.summary);
}

propgate.checks.run() is the same engine as propgate.dev and propgate check --remote. It stores nothing and schedules nothing — a domain you want watched over time is domains.create and then domains.check. propgate.health() is the other keyless call, and the only route that answers something other than the envelope; the client wraps it in one anyway, so you never have to know which routes are enveloped.

Types

Every resource shape is exported, written against the API's serialisers rather than against the database rows behind them. Timestamps are ISO 8601 strings and not Date objects, deliberately: this is the JSON that arrived, and reviving some fields into objects would make JSON.stringify(domain) produce something different from what came in.

import type { Domain, DomainState, Finding, PropgateResult } from "@propgate/sdk";

function needsAttention(domain: Domain): boolean {
  const failing: DomainState[] = ["degraded", "failed"];

  return failing.includes(domain.state);
}

function firstError(findings: readonly Finding[]): Finding | undefined {
  return findings.find((finding) => finding.severity === "error");
}

What is covered

Domainsdomains.create, list, listAll, get, update, check, timeline, remove
Profilesprofiles.create, profiles.get
Webhookswebhooks.create, list, get, update, remove, rotateSecret, listDeliveries, listAllDeliveries
Keys and membersapiKeys.create, apiKeys.list, apiKeys.revoke, members.list
Checkschecks.run, health

Opening an account

There is no signup call here, and that is deliberate. Signup is a mailbox flow — a six-digit code goes out and comes back to mint the first key — and by the time you are holding a server-side SDK you already have a key. Use the CLI or POST /v1/signup once, then keep the key in PROPGATE_API_KEY.