API reference › Domains › Update domain
Update a domain
/v1/domains/:idpropgate domains update <id> [--expect <requirement>.<field>=<value>] [--profile <key>]Changes what a domain is judged against: the values it supplies, the profile it is pinned to, or both. This is how you rotate a DKIM key, and how a customer moves from one profile to another.
Supply at least one of the two. A request that changes nothing would still reset the domain and re-verify it, which is a no-op with a side effect.
| Field | Type | Description |
|---|---|---|
| expectations | object | The complete set of values for this domain, keyed by requirement key then field. A full replace, not a merge — send every value the profile requires, not just the one that changed. |
| profile | string | A profile key to re-point this domain at. Resolves to that profile's current version. May name a different profile entirely, not only a newer version of the same one. |
curl -s -X PATCH https://api.propgate.dev/v1/domains/019fcf7a-2b3c-7d4e-9f5a-6b7c8d9e0f1a \
-H "authorization: Bearer pg_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H 'content-type: application/json' -d '{
"expectations": {
"dkim": { "expectedPublicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...NEW" }
}
}'{
"data": {
"createdAt": "2026-08-03T12:00:00.000Z",
"expectations": {
"dkim": { "expectedPublicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...NEW" }
},
"expectationsFingerprint": "3f786850e387550fdab836ed7e6dc881de23001b...",
"externalId": "cust_1",
"id": "019fcf7a-2b3c-7d4e-9f5a-6b7c8d9e0f1a",
"lastCheckedAt": "2026-08-05T09:14:02.000Z",
"name": "yourdomain.dev",
"object": "domain",
"profileVersionId": "019fcf6b-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"requirements": [],
"requirementsMet": 0,
"requirementsTotal": 0,
"state": "pending",
"verdict": "pass"
},
"error": null,
"meta": {
"profileVersionId": "019fcf6b-1a2b-7c3d-8e4f-5a6b7c8d9e0f"
}
}What it does to the domain
The domain returns to pending, its consecutive-failure count resets to zero,
and it becomes due immediately. state in the response says the first part;
the third is why you do not have to do anything else.
That last one is not incidental. A verified domain is scheduled a day out, so
leaving its schedule alone would make the reset cosmetic — the row would read
pending while nothing looked at the value you just sent for up to
twenty-four hours. The next tick picks it up instead, on the 30-second pending
cadence, because the fast-pending window is measured from this change.
Both are deliberate, and both are about not lying. The previous verdict answered a
different question, so it is not evidence about this one — and pending is
literally true: you have issued a new credential and nothing has verified the
domain against it yet.
No webhook fires. A domain.failed is a claim about your customer's DNS, and
this changed because you changed it. Without the reset, the next check would
compare a freshly issued key against a zone that has not been updated yet, read
one definite failure, and page somebody — and across ten thousand domains
mid-rotation that is ten thousand false pages inside one sweep interval.
Nothing is appended to the timeline for the first check afterwards, for the same reason. "The DKIM record changed Tuesday at 14:02" is the sentence the timeline exists to make true, and writing it about a zone that did not move would make the whole surface untrustworthy. The check after that resumes normally, with the new value as its baseline.
expectationsFingerprint on the stored result is how you tell whether a check has
looked at the new values yet: it is a digest of what the last check actually
compared, so a pass recorded before the rotation has a different one.
Re-pointing to another profile
curl -s -X PATCH https://api.propgate.dev/v1/domains/019fcf7a-2b3c-7d4e-9f5a-6b7c8d9e0f1a \
-H "authorization: Bearer pg_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H 'content-type: application/json' -d '{"profile":"full-mail"}'An upgrade from sending-only to full mail is the same operation as a rotation — "judge this domain against something else now" — so it takes the same path and the same reset.
Values are validated against the effective pair: the new profile, and the values you sent if you sent any, otherwise the ones already stored. A re-point the stored values cannot satisfy is refused and the domain is left exactly as it was:
{
"data": null,
"error": {
"message": "profile \"full-mail\" requires expectations.dkim-2.expectedPublicKey, which was not supplied"
},
"meta": null
}Writing it and letting the next sweep find the gap would turn a fixable 422 into
a domain stuck at indeterminate — and at pending, so it would look like it was
merely still being verified.
Values left over from a previous profile are kept, not pruned. They are inert: the compile ignores anything the current profile did not ask for. Pruning them would make re-pointing lossy, so going back would mean re-sending values you had already given us.
Rotating without a failure window
For a rotation with no window at all, add a second selector rather than swapping a value:
- Write a new profile version with a second
dkimrequirement for the new selector. PATCHthe domain onto it, supplying both keys.- Retire the old requirement in a third version once DNS has caught up.
Both keys are legitimately correct while the old TTL expires, and dkim is a
repeatable requirement type precisely so this works. Swapping in place is simpler
and usually fine — the reset above means it costs a re-verification rather than a
false alarm — but it does mean the domain reads pending until the zone catches
up.
Errors
A request with neither field is a 422:
{
"data": null,
"error": {
"message": "supply expectations, profile, or both"
},
"meta": null
}A profile that does not exist is a 422. An id that does not exist, or belongs
to a different account, is a 404 — and nothing is written.