propgate docs

API reference › DomainsUpdate domain

Update a domain

PATCH/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.

FieldTypeDescription
expectationsobjectThe 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.
profilestringA 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:

  1. Write a new profile version with a second dkim requirement for the new selector.
  2. PATCH the domain onto it, supplying both keys.
  3. 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.