API Explorer

On Behalf Of

When a User grants a here to an application, such as an AI agent, the application calls OBP under an identity of its own: a consent user, which OBP creates for that Consent. A call made that way has two Users behind it. user_id is the caller, the consent user that authenticated. on_behalf_of_user_id is the User the call was made for, the one who granted the Consent. When a User calls OBP for themselves, the two are the same User.

OBP finds the User acted for itself, from the Consent the caller authenticated with. It never takes it from a request body, header or query parameter, so a caller cannot name somebody else to act for.

This matters whenever OBP stores a user id. A consent user belongs to its Consent, so a row stored against it, such as an account holder, belongs to an identity that ends with the Consent, and the User who asked for it can no longer see it. So every database column that stores a user id has a policy, which says whose id to store when a consent user makes the call:

  • the caller: store the consent user's own id. This suits audit records of who acted, and the rows that carry a Consent's own permissions, which are meant to end with it.
  • the User acted for: store the id of the User who granted the Consent, so the row outlives the Consent. A few tables keep both ids, for example a counterparty or a transaction request, where who made the call is worth keeping too; the Note column says so.
  • refused: a consent user must not do this at all, because what it would create (a Consent, a Consumer or a token) outlives the Consent and would let the delegation extend itself.

A request that explicitly names a consent user as the target User, for example in a user_id field or a USER_ID path segment, is answered with 400 InvalidUserId.

Where a v7.0.0 response publishes both ids, it names them user_id and on_behalf_of_user_id, as in the record metadata of a here record.

Every column that stores a user id

Policy is whose id the column should hold when a consent user makes the call. Today is whose id it holds on this instance now. Of the 73 columns whose policy is "the User acted for" or "refused", 13 apply it today. "Not written by a consent user" means no API call writes that column with a consent user's id, because the endpoint refuses the consent user first or no endpoint writes the table.

Record Column Policy Today Note
AccountAccess user_fk the caller the caller views copied from the consent JWT each request; has lifecycle GC
Entitlement mUserId the caller the caller only when createdByProcess == consent_user: the consent engine copying the consent's own scope
Entitlement mGrantedByUserId the caller the caller audit: who granted
UserLocks UserId the caller the caller lock the authenticated user
ExpectedChallengeAnswer ExpectedUserId the caller the caller consent and signing-basket authorisation: the caller IS the person authorising, so the challenge is theirs
ExpectedChallengeAnswer ExpectedUserId the User acted for the User acted for payment SCA: the challenge belongs to the human whose money moves, never to the agent that started the payment
ChatMessage SenderUserId the caller the caller sender = the authenticated user is truthful
Metric userId the caller the caller record both: on-behalf-of via consent_reference_id at read time
MetricArchive userId the caller the caller as Metric_UserId
ConnectorTrace userId the caller the caller as Metric_UserId
DynamicDataAccess GrantedBy the caller the caller audit: who granted
AuthUser user the caller the caller login row -> its own ResourceUser; not attribution
OpenIDConnectToken AuthUserPrimaryKey the caller the caller token belongs to the login; not attribution
UserRefreshes mUserId the caller the caller operational: refresh of the authenticated user's own account list
PlatformApp MarkedByUserId the caller the caller audit: who marked the Consumer as a platform app
GroupMembership CreatedByUserId the caller the caller audit: who added the member, as Entitlement_GrantedByUserId
AssetStatusHistory ChangedByUserId the caller the caller audit: who changed the asset's status, the question a regulator asks
TransactionRequest mUserId, mOnBehalfOfUserId the User acted for the User acted for record both: mUserId = userId, mOnBehalfOfUserId = onBehalfOfUserId
Entitlement mUserId the User acted for the User acted for the role holder; the consent-engine case is Entitlement_UserId_ConsentScope
GroupMembership UserId the User acted for not written by a consent user the member, who holds the Roles the Group grants; as Entitlement_UserId
AccountHolders user the User acted for the User acted for the human holds the account; one held by a per-consent identity strands when the consent dies
UserCustomerLink mUserId the User acted for the User acted for a Customer is linked to a human; a link on an agent identity dies with its Consent
AccountApplication mUserId the User acted for the caller explicit target: user_id comes from the request and is guarded at the endpoint, so the provider redirect is unreachable -- see ON_BEHALF_OF_USER_ID_PLAN.md row 14
AccountAccessRequest RequestorUserId the User acted for the caller who asked for access; the request outlives the session it was made in
AccountAccessRequest TargetUserId the User acted for not written by a consent user explicit target: a consent user named here is rejected at the endpoint
AccountAccessRequest CheckerUserId the User acted for the caller who approved; maker/checker evidence has to name a human
DynamicChangeRequest RequestorUserId the User acted for the caller maker of a dynamic-code change
DynamicChangeRequest CheckerUserId the User acted for the caller checker; must differ from the requestor
EntitlementRequest mUserId the User acted for the caller who asked for the role; the grant that follows lands on a human
UserScope mUserId the User acted for the caller the scope holder
ApiCollection UserId the User acted for the caller the user's own saved collection, created through POST /my/api-collections
UserAttribute UserId the User acted for the caller the user's own attribute, created through the /my/ endpoints
UserAgreement UserId the User acted for the caller the user's own acceptance of terms
UserInitAction UserId the User acted for the caller the user's own onboarding action
UserAuthContext mUserId the User acted for the caller consent copies the on-behalf-of user's contexts into ConsentAuthContext separately
UserAuthContextUpdate mUserId the User acted for the caller as UserAuthContext_UserId
DynamicEntity UserId the User acted for the User acted for the definition's creator; a definition outlives the Consent that created it
DynamicData UserId the User acted for the User acted for personal rows, and the one reference where the redirect MUST be symmetric: MapppedDynamicDataProvider resolves on save/update/get/delete alike, because a row keyed by this column on both sides is otherwise written by an agent and then invisible to it
DynamicData CreatedByUserId, CreatedByOnBehalfOfUserId the User acted for the User acted for record both: who made the call that created the record, agent included, and the person it was made for; published in the v7.0.0 record metadata
DynamicData UpdatedByUserId, UpdatedByOnBehalfOfUserId the User acted for the User acted for record both: as DynamicData_CreatedByUserId, for the call that last saved the record
DynamicDataAccess UserId the User acted for the caller row-level ACL. Deliberately still on the consent user today, because the bootstrap grant and the allows check have to agree with each other -- rows strand, nothing leaks. A later Phase 2 row; see the plan
DynamicEndpoint UserId the User acted for the caller the dynamic endpoint's creator; outlives the Consent
DynamicResourceDoc CreatedByUserId, UpdatedByUserId the User acted for the caller and UpdatedByUserId; a dynamic artefact outlives the Consent that created it
DynamicMessageDoc CreatedByUserId, UpdatedByUserId the User acted for the caller and UpdatedByUserId; a dynamic artefact outlives the Consent that created it
ConnectorMethod CreatedByUserId, UpdatedByUserId the User acted for the caller and UpdatedByUserId; a dynamic artefact outlives the Consent that created it
AbacRule CreatedByUserId, UpdatedByUserId the User acted for the caller and UpdatedByUserId; an access rule outlives the Consent that created it
Counterparty mCreatedByUserId, mCreatedByOnBehalfOfUserId the User acted for the User acted for record both: a counterparty controls where money may be sent, so the actor stays on mCreatedByUserId (and is published as created_by_user_id) while the human goes to mCreatedByOnBehalfOfUserId
CounterpartyWhereTag user the User acted for the caller who tagged the counterparty's location
ApiProductSubscription CreatedByUserId the User acted for the caller a subscription outlives the Consent that took it out
DynamicGlossaryItem CreatedByUserId the User acted for the caller outlives the Consent that created it
DomainApi CreatedByUserId the User acted for the User acted for the Domain API's creator; a published API outlives the Consent that created it
DynamicEntityWebhook CreatedByUserId, CreatedByOnBehalfOfUserId the User acted for the User acted for record both, like Counterparty: a webhook sends data out of OBP and outlives the Consent that made it, so the agent stays on CreatedByUserId (published as created_by_user_id) and the person it acted for goes to CreatedByOnBehalfOfUserId
Bank CreatedByUserId the User acted for the User acted for creator grant already resolved at the endpoint
Organisation CreatedByUserId the User acted for the caller outlives the Consent that created it
Asset CreatedByUserId the User acted for not written by a consent user outlives the Consent that created it, as RoutingScheme_CreatedByUserId; seeded rows hold system:asset-seed
File UserId, OnBehalfOfUserId the User acted for not written by a consent user record both: UserId = who uploaded (the agent), OnBehalfOfUserId = the human, who owns the file
FileAttachment UserId, OnBehalfOfUserId the User acted for not written by a consent user record both: UserId = who attached (the agent), OnBehalfOfUserId = the human
FileAccess GrantedByUserId, OnBehalfOfUserId the User acted for not written by a consent user record both: GrantedByUserId = who granted (the agent), OnBehalfOfUserId = the file's owner it acted for
FileAccess GranteeUserId the User acted for not written by a consent user explicit target: the User given access is named in the request; access given to a consent user would die with its Consent
PayeeLookup CreatedByUserId the User acted for the caller outlives the Consent that created it
RoutingScheme CreatedByUserId the User acted for the caller outlives the Consent that created it
UtilityPaymentCallback CreatedByUserId the User acted for the caller outlives the Consent that created it
StandingOrder UserId the User acted for the caller the payer; a standing order keeps executing long after any Consent expires
DirectDebit UserId the User acted for the caller the payer; a mandate keeps executing long after any Consent expires
Mandate CreatedByUserId, UpdatedByUserId the User acted for the caller and UpdatedByUserId; a mandate authorises payment and outlives the Consent that created it
SignatoryPanel UserIds the User acted for the caller list of user ids
AccountWebhook mCreatedByUserId the User acted for the caller published as created_by_user_id on v3.1.0 and v4.0.0; nothing reads it as an ownership key, so the human is not locked out -- but a webhook outlives the Consent that made it and keeps sending account events, so the human behind it should be recorded. Record-both, like Counterparty. See todo/webhook_attribution.md
SystemAccountNotificationWebhook CreatedByUserId the User acted for the caller as AccountWebhook_CreatedByUserId
BankAccountNotificationWebhook CreatedByUserId the User acted for the caller as AccountWebhook_CreatedByUserId
ChatRoom CreatedByUserId the User acted for the caller Portal chat: a human's room
Participant UserId the User acted for the caller the person in the room
Reaction UserId the User acted for the caller the person who reacted
ChatEmailDigestState UserId the User acted for the caller the person the digest is for
ChatMessage MentionedUserIds the User acted for the caller explicit targets, humans by construction
CrmEvent mUserId the User acted for the caller the user the event concerns
KycCheck user the User acted for the caller the customer's user
KycCheck mStaffUserId the User acted for the caller staff = human operator
KycDocument user the User acted for the caller the customer's user
KycStatus user the User acted for the caller the customer's user
SocialMedia user the User acted for the caller the customer's user
CustomerMessage user the User acted for the caller the customer's user
Meeting mCustomerUserId the User acted for the caller the customer side of the meeting
Meeting mStaffUserId the User acted for the caller staff = human operator
Tag user the User acted for the caller the author of the annotation; it outlives the Consent
WhereTag user the User acted for the caller the author of the annotation; it outlives the Consent
TransactionImage user the User acted for the caller the author of the annotation; it outlives the Consent
Consent mUserId refused the caller (not refused) a consent user creating a consent = nested delegation
Consumer createdByUserId refused the caller (not refused) credentials outlive the consent
Token userForeignKey refused the caller (not refused) credentials outlive the consent