SDK › Overview
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/sdkimport { 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 pageThe 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 firstThe 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
| Domains | domains.create, list, listAll, get, update, check, timeline, remove |
| Profiles | profiles.create, profiles.get |
| Webhooks | webhooks.create, list, get, update, remove, rotateSecret, listDeliveries, listAllDeliveries |
| Keys and members | apiKeys.create, apiKeys.list, apiKeys.revoke, members.list |
| Checks | checks.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.