SDK › domains
Domains
Register, verify, read, delete. Registering and verifying are separate calls, and the separation is what keeps a bulk import from being a DNS storm.
const { data, error, meta } = await propgate.domains.create({
name: "yourdomain.dev",
profile: "sending",
externalId: "cust_1",
expectations: {
dkim: { expectedPublicKey: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A..." },
},
});
data?.state; // "pending" — registering does not touch DNS
meta?.created; // false means this externalId already existed, and nothing was writtenexternalId is your retry key. Sending one you already used returns the domain
that exists rather than erroring, and meta.created tells "the customer I
already had" from "a new one" without a mapping table of your own. Because that
branch writes nothing, it is also not how you rotate a value.
Verifying
const { data, error, meta } = await propgate.domains.check("019fcf7a-2b3c-7d4e-9f5a-6b7c8d9e0f1a");
data?.state; // "pending" | "verifying" | "verified" | "degraded" | "failed"
data?.requirements?.filter((requirement) => !requirement.satisfied);
// True when the domain's configuration changed while the check was running, so
// the verdict was discarded. The row comes back as it now stands.
meta?.superseded;propgate.domains.check() runs the checks now and stores what it found. It is
limited to 100 a minute per account, because each call aims real queries at
somebody else's authoritative servers — see
rate limits you have to schedule around.
Continuous re-checking is the sweeper's job and does not
come through here.
Changing what a domain is judged against
// A DKIM rotation. Never a second create: that path is idempotent and would
// answer 200 having written nothing.
const { data } = await propgate.domains.update("019fcf7a-2b3c-7d4e-9f5a-6b7c8d9e0f1a", {
expectations: { dkim: { expectedPublicKey: rotatedKey } },
});
data?.state; // back to "pending" — nothing has judged the new value yetpropgate.domains.update() takes expectations, profile, or both — a request
that changes neither is refused, because it would still reset the domain to
pending and re-verify it for nothing.
Re-pointing to another profile is the same operation: both say "judge this
domain against something else now", and both invalidate the previous verdict.
Neither fires a webhook. The domain going back to pending is news about you,
not about the customer's DNS, and paging ten thousand people over a key you
rotated is exactly the failure hysteresis exists to
prevent.
Listing, and reconciling
// One page, and the cursor for the next.
const page = await propgate.domains.list({ state: "failed", limit: 200 });
// Or every page, 200 rows a request, stopping on a failure rather than
// returning a short list.
const { data, error } = await propgate.domains.listAll({ state: "failed" });
if (error !== null) {
// Half a reconciliation is worse than none: this is a failure, not an
// empty account.
throw error;
}propgate.domains.listAll() follows meta.nextCursor to the end at the largest
page the server will give — 200 rows a request, so ten thousand domains is fifty
round trips, comfortably inside the per-account rate limit.
It returns a failure mid-walk rather than the pages it managed to collect. A short list with no error is indistinguishable from an account with three domains, which is the kind of wrong answer a reconciliation loop acts on.
import { Propgate } from "@propgate/sdk";
const propgate = new Propgate(process.env.PROPGATE_API_KEY);
export async function reconcile() {
const { data, error } = await propgate.domains.listAll();
if (error !== null) {
throw error;
}
for (const domain of data) {
await upsertCustomerDomain({
customerId: domain.externalId,
lastCheckedAt: domain.lastCheckedAt,
state: domain.state,
unmet: (domain.requirementsTotal ?? 0) - (domain.requirementsMet ?? 0),
});
}
}Domains sort ascending by id, so a row created while the walk is in progress lands at the end and is included.
Reading one, and what changed
const { data } = await propgate.domains.get("019fcf7a-2b3c-7d4e-9f5a-6b7c8d9e0f1a");
// The derivation behind the stored verdict: which name, which server, what
// came back. Present on get and check, absent from the list.
data?.lookups;
// What actually changed, newest first. Two identical checks add nothing.
const timeline = await propgate.domains.timeline("019fcf7a-2b3c-7d4e-9f5a-6b7c8d9e0f1a", { limit: 50 });
await propgate.domains.remove("019fcf7a-2b3c-7d4e-9f5a-6b7c8d9e0f1a");propgate.domains.get() is a read against storage, never a fresh DNS run —
lookups is the derivation behind whatever the last check concluded, which is
what answers "why did you say that" about a disputed verdict.
propgate.domains.timeline() is what changed, not a log of every check. A
value that stays the same appends nothing, which is what keeps this a timeline
rather than 360,000 rows a day.
propgate.domains.remove() stops tracking. Without it the sweeper inherits
every domain your account ever had.