Invitations
Authorize, inspect, and end an Organization invitation with the Core SDK and the CLI.
An invitation offers exact authority in one Organization to one exact grantee. This guide covers the Core SDK and CLI surface that exists today.
Acceptance is the grantee's explicit consent
The exact grantee accepts an authorized offer with the offer's current digest. Consent is recorded
once, and the offer then moves to pending_grant while its grant is executed automatically by the
deployment's technical executor. Neither the grantee nor an Admin signs anything for the
acceptance itself. Read the settled active state through get or wait; see
Acceptance below.
The SDK surface
Every method returns Promise<CapxulResult<T>> and accepts the shared signal
options.
| Call | Result | Notes |
|---|---|---|
org(orgId).invitations.preview(input, options?) | InvitePreview | Read only. It writes nothing. |
org(orgId).invitations.invite(input, options?) | InvitationView | Authorizes one exact offer and executes its configuration hop. |
org(orgId).invitations.accept({ invitationId, expectedOfferDigest }) | InvitationView | The exact grantee consents; starts the automatic grant. |
org(orgId).invitations.list({ cursor?, limit?, phases? }) | InvitationPage | The Organization's invitations; Admin only. |
org(orgId).invitations.get({ invitationId }, options?) | InvitationView | One invitation in the named Organization. |
org(orgId).invitations.decline({ invitationId }) | InvitationView | The exact grantee ends the offer. |
org(orgId).invitations.cancel({ invitationId }) | InvitationView | The current Admin ends the offer. |
org(orgId).invitations.resend({ invitationId }) | InvitationView | Retries delivery of a verified pending offer. |
org(orgId).invitations.retry({ invitationId }) | InvitationView | Resumes the current offer state. |
orgs.invitations({ cursor?, limit?, phases? }, options?) | InvitationPage | Your own offers, with a versioned cursor. |
invite takes the bounded request:
type InviteInput = {
readonly requestKey: string;
readonly to:
| { readonly type: "email"; readonly email: string }
| { readonly type: "account"; readonly accountId: AccountId };
readonly grants: readonly InviteGrant[];
};preview takes the same request without requestKey. The key is write identity,
so it takes no part in a read. The bounds are 1–16 distinct authorities, at most
four new Budgets, 1–32 recipient Accounts per allowlist, and a requestKey of
1–128 printable ASCII characters.
The same requestKey replays the same invitation. A different request under the
same key is refused. Use a new key only when you intend a new offer.
The configuration hop
invite does not stop at the durable offer. After the write persists the
invitation and its configure command, the method drives that command through the
same human UserOperation owner the Permission and payment writes use: it prepares
the stored command for this Account's Safe, delivers the durable identity to
onPrepared, signs the returned digest with the client's signer, and submits it.
The client must therefore be execution-capable. In Node, compose it with the package's signer entry:
import { localPrivateKeySigner } from "@capxul/sdk/node";
const client = await createCapxulClient({
// …
signer: localPrivateKeySigner({ privateKey }),
});onPrepared receives { invitationId, commandId }. It runs after the prepared
authority has been checked and before any digest is signed, and the write awaits
it: a caller that cannot record the identity does not authorize the execution. A
client composed without a signer refuses invite with NOT_IMPLEMENTED before it
writes anything, because an offer that cannot be configured can never leave
authorizing.
On a fresh request the view invite returns is the authorization-time view:
authorizing, with its commandId and its configuration_pending recovery.
The offer reaches pending with authorizedAt and expiresAt only when the
configuration verification lands, after the call returns — a re-read here would
show the same authorizing view. Read the settled state through get or wait.
A settled replay is the exception: a requestKey whose invitation has already
left authorizing returns that durable current view unchanged, and composes no
signer.
The replay rules do not change: call invite again with the same requestKey.
The same invitation and the same stored execution are reused, and a prepared
response lost in transit is recovered by the same identity.
org(orgId).invitations.list is the Admin view of one Organization. It reads
every invitation of that Organization, including offers another Admin authored.
A current Admin gets the page; a member without management authority and a caller
outside the Organization get the existing typed refusal, never a page. The page
uses the same versioned cursor as the own-offer list, and every row is checked
against the Organization the bundle was resolved for.
orgs.invitations is the grantee projection. It never consults a saved
Organization: the session decides whose offers it returns.
The invitation lifecycle
InvitationView.phase is the durable state. The phases are authorizing,
pending, pending_grant, active, revoking, declined, cancelled,
expired, and revoked.
authorizingmeans the offer and its configuration command are durable, and the Admin's configuration transaction is not applied yet.pendingmeans the offer is authorized and can be accepted within its expiry.pending_grantmeans the grantee accepted:acceptedAtand the accepted digest are durable, and the automatic grant execution is in flight. It does not expire while it waits, and a provider outage leaves it here with consent intact.activemeans the grant receipt verified and the exact offered assignments were applied.- The four terminal phases are
declined,cancelled,expired, andrevoked.
recovery names the next responsible actor and action. delivery reports
not_confirmed, sent, or failed. sent means the configured email owner
durably accepted the message for delivery; it is not proof of inbox arrival, and
a later provider outcome does not change the field. resend attempts one
delivery message through the configured email provider and reports that
attempt's outcome: sent when the provider accepted the message, failed
otherwise. It does not extend expiry and does not change consent.
decline and cancel are idempotent for the same ending reason. decline
requires the exact grantee. cancel requires current Admin authority. Both
start cleanup. A request in a different state is refused with WRONG_STATE.
The offer carries a live policy, not a copy
An offered Budget resolves against the Permission identity that exists now. If
an Admin changes that same Budget policy before acceptance, the accepted
authority follows the current policy. The offerDigest on the view is the value
acceptance must name, and it changes when the policy changes. A different
Budget, a different asset, or a different grantee requires a newly authorized
invitation.
InvitationView.offers carries the current label and policy. grants carries
the exact public authorities. offerDigest covers asset, cap, action, and
destination facts. Labels, Account attachment, readiness, and timestamps are
excluded from the digest.
Account readiness is independent
InvitationView.grantee.accountReady is nullable, and it is independent of the
invitation's phase and of the authority being granted. true means the
grantee's Account is deployed and claimed. false means it is not. null means
no Account is attached yet, or its readiness is not known.
A grantee without an Account can hold an authorized or active invitation, and an
invitation can stay pending while the grantee's Account is ready. Readiness
never grants authority, and an offered authority never implies a ready signer.
Read accountReady for the signer question and phase for the authority
question; do not derive one from the other.
Trust boundary
An invited person never receives Admin authority through this surface. The grant application uses a dedicated technical executor with one-use rights, and that executor cannot change policy. The offer you authorize is the exact authority the grantee can receive.
Operational receipts and pending work
Invitation work is nonfinancial. The result shows the offer state: a
commandId for the configuration command, authorizedAt, acceptedAt, and
expiresAt. A transaction hash is not an accepted invitation, and an
authorizing or pending_grant phase is not active membership.
retry returns the current view or a typed dependency failure. It never creates
configuration, consent, a generation, or a logical grant.
Acceptance
org(orgId).invitations.accept({ invitationId, expectedOfferDigest }) is the
exact grantee's consent commit. expectedOfferDigest is the offerDigest the
caller read for that invitation: get and the own-offer list show it, and
nothing else may be substituted for it.
Consent is recorded once, in one transaction that also creates the offer's grant command and hands that command to the execution lane:
- A repeat with the stored digest returns the same accepted result and writes
nothing. A different digest is refused with
WRONG_STATEinvitation-offer-changed; it can never overwrite the stored consent. - The wrong authenticated grantee, an offer that is not
pending, and an offer at or past itsexpiresAtall refuse before any consent is written. Acceptance requiresnow < expiresAt, so an offer that expired unaccepted cannot be brought back by accepting it. - The write signs nothing. The grant transaction is executed by the deployment's technical executor with its one-use rights after the consent commits, so the grantee never holds a key for it and no Admin returns to help. An Admin returns only when the offer must change.
The view the call returns is the authorization-time view of the consent
commit: pending_grant, with acceptedAt and the accepted digest durable. The
offer reaches active only when the grant receipt verifies and the exact
assignments are applied, which happens in the lane the commit started. Read the
settled state through get or wait. A repeat on an offer whose consent is
already recorded returns that durable current view unchanged.
Until acceptance is recorded:
- An authorized offer stays unaccepted. It is not membership.
- The exact grantee can read and decline the offer through their own offers.
- An Admin can read, cancel, and resend it.
- Do not build an integration that assumes a member becomes active.
The CLI
The CLI calls the same SDK methods. It holds no route of its own.
capxul org invite list [--mine] [--limit N] [--cursor C] [--phase PHASE ...] [--org O]
capxul org invite get --invitation-id I [--org O]
capxul org invite wait --invitation-id I [--org O] [--timeout-seconds N]
capxul org invite review --invitation-id I --org O [--timeout-seconds N]
capxul org invite accept --invitation-id I --offer-digest D --org O [--confirm] [--timeout-seconds N]
capxul org invite decline --invitation-id I --org O [--confirm]
capxul org invite cancel --invitation-id I --org O [--confirm]
capxul org invite resend --invitation-id I --org O [--confirm]
capxul org invite retry --invitation-id I --org O [--confirm]
capxul org member invite (--to-email E | --account-id ID) [--budget-id B ...] [--permission-id M ...] [--new-budget-name N --asset A (--limit Q | --unlimited) (--recipient-account ID ... | --any-recipient) --actions pay[,commitments]] --org O [--request-key K] [--preview] [--confirm]org invite list returns your own offers. With --org O it returns that
Organization's invitations instead, for a current Admin. The result carries
invitations, nextCursor, and observedAt: pass nextCursor back as
--cursor to read the next page. --mine names the own-offer projection, which
is already the default; it refuses together with --org, because the
Organization list is not filtered to you.
org invite get and org invite wait use --org when you name it. Without
--org, they walk your own-offer pages until they find the invitation's own
row. They never consult a saved read default, and --timeout-seconds bounds
their resolution together with the read.
org invite wait only reads. It finishes when the offer settles or when the
recovery asks a person to act. It never retries, resends, or accepts.
org invite review requires the exact --org and returns the current
InvitationView directly in data. It performs no write or signing.
org invite accept needs --org and the exact digest the offer shows as
--offer-digest. A noninteractive run must supply --offer-digest and
--confirm; without the digest it refuses with a usage error before it creates a
client, because it will not choose a digest for you. An interactive run may omit
it: the command reads the offer, shows the digest it read, and fills it from that
displayed value behind the standard confirmation prompt. A digest you state is
never replaced by the displayed one — if they differ, the backend refuses and
nothing is written. --expected-offer remains a compatibility spelling; if
both spellings are present they must match. The command returns the
InvitationView directly in data. It signs nothing, so it carries no browser-bridge
requirement: any authenticated grantee session can run it, including a headless
or CI session.
org member invite --preview writes nothing and needs no request key. A
noninteractive authoring write needs --request-key and --confirm. An
interactive run prints the request key it used before it submits, so a lost
response is recoverable with the same identity. It also executes the
configuration hop, and prints invitation.prepared with the durable
{invitationId, commandId} pair before any signature is forwarded.
The CLI signs that hop through its browser bridge, exactly like the other
Organization writes; the browser opens only after you confirm. This is the only
signer the installed CLI carries, so a CLI-driven org member invite needs an
Admin whose Account is browser-owned. For a headless or CI-provisioned
Organization, call the SDK from Node with an injected signer (see The
configuration hop); the CLI has no private-key flag, by
design.
Then it uses the same result envelope and exit codes as every other command.
NOT_AUTHENTICATED returns exit 3, ROLE_PERMISSION_DENIED returns exit 4, a
usage or state refusal returns exit 2, and a dependency failure returns exit 5.
Related
- Create a payment for the Budget that an offer can name.
- Observe SDK failures for the operation record every invitation call writes.