Operating it

Revocation

Withdrawing a binding: the console's Revoke a participant panel, which opens against the participant selected in the Participants table, what "revoked" means to everyone downstream, and how long it takes to bite.

Revocation is the operation the whole design is judged on. Admitting someone is easy and reversible; removing them is neither, and a registry that cannot credibly remove an identity is a phone book, not a trust anchor.

Nothing is deleted

There is no delete. Revocation appends a new entry saying the previous binding no longer holds, and the record stays in the log with state: revoked and a reason.

This is not squeamishness about data. It is the requirement:

So a revoked record still resolves. include_revoked=true returns it with its state, ?version_id= returns any earlier version, and as_on (an RFC 3339 time, or a YYYY-MM-DD date meaning the end of that day in UTC) answers what was true at any instant that is over. An as_on of today, of a time still ahead, or less than a minute ago is a question about now: it gets the same 404 as a plain read unless it also sends include_revoked=true. A misspelt parameter is a 400, not a silently ignored one: ?includeRevoked=true will not quietly return the live view. See API.

What downstream actually does with it

Two things, and only the second is automatic:

Signature validation stops. A validator that re-fetches the key sees the record is not live and refuses. This is the mechanism that matters.

Discovery stops returning them. Since the domain query landed, a revoked participant is no longer returned as a destination — see discovery. Before that, revocation meant "stops being verifiable" but not "stops being routed to", which is a strictly weaker claim than it sounds.

Note what revocation does not do: it does not reach into anybody's cache, and it does not stop a counterparty who never re-checks. Which brings us to timing.

How long revocation takes

Measured end to end against two live beckn-onix v1.8.0 adapters, with the participant record declaring ttl: 20:

t+00s  search accepted (adapter still holds the cached key)
t+05s  search accepted
t+10s  search accepted
t+15s  search REJECTED -> HTTP 401

Fifteen seconds during which a compromised key kept validating real traffic.

TTL bounds that window. It does not remove it, and it is the only thing bounding it unless push is configured, which delivers the revocation instead of waiting for a timer.

Lowering TTL is not a free fix: it trades the exposure window for lookup load on every validator on every message.

Doing it

The write is conditional, like every other. Read the current version tag and send it, so a concurrent change fails loudly rather than being clobbered:

echo '{"reason":"key compromised, reported by operator 2026-08-14"}' > reason.json

t=$(curl -s "$DEDI/dedi/lookup/$NS/$REG/$NAME?include_revoked=true" | jq -r '.data.version_tag // empty')

dedid sign -key publisher.key -kid op-1 \
  -method POST -path /admin/namespaces/$NS/registries/$REG/records/$NAME/revoke \
  -body reason.json -if-match "$t" > headers.txt

curl -s -u "admin:$DEDI_ADMIN_PASSWORD" -X POST \
  "$DEDI/admin/namespaces/$NS/registries/$REG/records/$NAME/revoke" \
  -H @headers.txt -H 'Content-Type: application/json' --data-binary @reason.json

The body is optional and is just {"reason": "…"}. The revoked version carries the previous payload forward with revocation_reason added, so the record stays inspectable at the version that withdrew it. Revoking an already-revoked record is a no-op.

Write a real reason

The reason is published and permanent, and it is the only part of a revocation a future reader cannot reconstruct from the log itself. "compromised" tells them nothing. What happened, who reported it, and when — that is what someone auditing this in a year needs, and they cannot ask you.

Revoke every binding, not one

A subscriber may hold several keys, each its own record (onboarding). Revoking one binding leaves the others live.

That is correct behaviour and a real trap. On the demo network a participant existed twice — once under its key id, once under its subscriber_id — with identical payloads. Signature validation only ever found the key-named record, but discovery matches on the payload and ignores the name, so it returned both. Revoking the key-named record left the other live and still being handed out as a destination.

So before revoking, list what the subscriber actually holds:

curl -s "$DEDI/dedi/query/$NS/$REG?page_size=100" \
  | jq -r '.data.records[].record_name' \
  | while read n; do
      curl -s "$DEDI/dedi/lookup/$NS/$REG/$n" \
        | jq -r --arg n "$n" '"\($n)\t\(.data.details.subscriber_id)\t\(.data.state)"'
    done

Anything sharing the subscriber_id you are removing needs revoking too.

Verifying it worked

Not "the write returned 200". Check the three things that are separately true:

# 1. the record reports revoked, and is still there
curl -s "$DEDI/dedi/lookup/$NS/$REG/$NAME?include_revoked=true" | jq '.data.state'

# 2. it is gone from discovery
curl -s "$DEDI/dedi/query/$NS/$REG?domain=$DOMAIN" | jq '.data.participants[].subscriber_id'

# 3. a real signed message from that key is now refused by a counterparty

(3) is the one that counts, and the only one that measures the property revocation exists to deliver. The other two say the registry changed its mind; only the third says the network did.

Undoing one

Publish a new binding. There is no "un-revoke", because the revocation happened and the log says so permanently. Bringing a participant back is a new statement about the future, not an edit to the past — which is what you want, since the readers who need to know they were once removed are exactly the ones who would be misled by it disappearing.

← All documentation