Contents

Revoke entitlement

Remove a user's ownership of a SKU; declines with NOT_ENTITLED when they never owned it.

Source src/operations/entitlements.ts#L69-L90revokeEntitlementsrc/operations/registry.ts#L66REGISTRY

API revokeEntitlement

revokeEntitlement removes a user’s ownership of a SKU, the mirror of grantEntitlement. Ownership is a record, not a balance. No money moves, and the ledger is untouched.

You name the user and the SKU, and optionally a reason for the audit trail:

const outcome = await economy.submit({
  kind: 'revokeEntitlement',
  idempotencyKey: 'idem_revoke_1',
  actor: { kind: 'system', service: 'fulfillment' },
  userId: 'usr_owner',
  sku: 'wrld_pass',
  reason: 'chargeback',
});
// → { status: "committed", transaction: { id: "txn_…", … } }
curl -s https://economy.example/submit \
  -H 'content-type: application/json' \
  -d '{
    "kind": "revokeEntitlement",
    "idempotencyKey": "idem_revoke_1",
    "actor": { "kind": "system", "service": "fulfillment" },
    "userId": "usr_owner",
    "sku": "wrld_pass",
    "reason": "chargeback"
  }'

Once this commits, read.entitled("usr_owner", "wrld_pass") returns false. Any UI that gates access on that SKU stops granting it.

Parameters

The payload fields, beyond the kind tag, are:

FieldTypeDefaultDescription
idempotencyKeystring(required)A retried submit with the same key runs at most once. See idempotency.
actorPrincipal(required)Who is asking. Must be system or operator, see Authorization.
userIdstring(required)The user losing ownership. A blank or whitespace-only value throws.
skustring(required)The item or feature code to revoke, such as "wrld_pass". A blank or whitespace-only value throws.
reasonstringOptional note for the audit trail; omitted when not given.

Returns

revokeEntitlement resolves to an Outcome.

  • committed: the user owned the SKU and the ownership record was dropped.
  • duplicate: a request with the same idempotencyKey already ran; the earlier transaction is returned unchanged.
  • rejected: the user did not own the SKU. The only RejectionCode here is NOT_ENTITLED.

The committed transaction is a lifecycle marker. It carries a fresh id and commit time. Its legs and links lists are empty, because nothing posted to the ledger.

Postings

None. Entitlements track ownership only, so revokeEntitlement writes no double-entry posting and touches no account.

The handler drops the ownership record through unit.entitlements.revoke. The returned transaction carries empty leg and link lists.

Authorization

revokeEntitlement is platform-initiated only. An end user (actor.kind === "user") may never run it, see actors and authorization.

A revoke names an arbitrary userId the caller need not own. It also posts no debit the ownership check could catch. So it is gated up front: only a system service or a human operator may call it.

A user actor is declined with an AUTH.UNAUTHORIZED fault before any work begins.

Reason codes

revokeEntitlement returns one RejectionCode:

CodeWhen
NOT_ENTITLEDThe user does not currently own sku, so there is nothing to revoke.

The rejection carries the userId and sku in its detail, so the caller knows which ownership check came back empty.

A malformed request throws instead:

CodeWhen
OP.MALFORMEDA blank or whitespace-only userId or sku.

A blank field would revoke against a phantom user or SKU, so a malformed request surfaces as a client error rather than a silent NOT_ENTITLED.

Preconditions and invariants

The handler checks ownership before it revokes. It calls entitlements.owns(userId, sku) and returns NOT_ENTITLED when the user does not hold the SKU.

Revoking is idempotent through the idempotencyKey. A retry under the same key returns the first result as duplicate rather than re-running.

The ledger’s money invariants (conservation, no-overdraft, and backing) are unaffected, because this operation moves no money.

See also