About Bridges
Bridge represents a ledger configuration record that is used to register remote services with the ledger. This is an optional record in most cases, since remote service calls can usually be specified inline, for example actions in effects, aspects, etc. An example of a bridge is an integration service that connects with a banking core in order to perform debit and credit operations in response to ledger balance movements. Other use cases include services which host webhooks in order to subscribe to ledger events and react to them.
Bridges are here to centralize this concept and make it easier to manage external integrations. The most important value of a bridge is that it is possible to easily manage all integrations in one place. Another benefit is avoiding the need to repeat the same information in multiple places. Data like security configuration, base URLs and similar can be registered only in a bridge record, and you can reference this configuration much more easily this way.
Bridges are created the same way as all other ledger records, they have their handle and some additional properties that is specific to this record type. Specific properties of bridges are:
- schema - defines the communication schema used by the bridge, for example
rest - config - a base config that is shared across all bridge functionalities, defines the base URL.
- secure - defines security mechanisms for accessing the bridge, this includes OAuth and similar configurations
Here is an example of registering a bridge that exposes REST API endpoints:
{
"handle": "bank1",
"config": {
"server": "https://example.com/v2"
},
"traits": [
'debits',
'credits'
],
"secure": [{
"schema": "oauth",
"client": "<OAuth client id>",
"secret": "<OAuth client secret>"
}, {
"schema": "mtls",
"public": "<MTLS public key>",
"secret": "<MTLS private key>"
}],
"custom": {},
"schema": "rest",
"access": [
{
"action": "any",
"signer": {
"public": "<bridge signer public key>"
}
},
{
"action": "read",
"bearer": {
"sub": "<bridge signer public key>"
}
}
]
}💡 Note that the server url has /v2 prefix in path which represents the current version of ledger protocol. It is the recommended to use versioning of rest endpoints on bridge so that newer versions of protocol can be easily supported in the future.
All examples below will use /v2 path prefix on bridge endpoints, which means that registered bridge server configuration should have this prefix. Ledger will not prepend it automatically.
By creating a Bridge that uses the rest schema, the interface below has to be exposed on the registered URL.
Rest interface consists of these types of endpoints:
- Record endpoints
POST /v2/<record-plural>for creating the record on bridge side, record signed by ledger key is received in the HTTP bodyPUT /v2/<record-plural>/:handlefor updating the record by handle or creating if it doesn’t exist yet, new state of record signed by ledger key is received in HTTP body
- Command endpoints
POST /v2/<record-plural>/:handle/<action>for executing an action on the record, command record which contains the handle and action signed by ledger key is sent in HTTP body
- Event endpoints
POST /v2/effects/:handleused to receive events which originate from the registered effect withhandle, see details about effects
- Anchor endpoints
GET /v2/anchorsused to resolve anchors on the bridge side. This endpoint can only be called if the bridge implementsanchorstrait. See details About AnchorsPOST /v2/anchors/!lookupused to resolve anchors on the bridge side with additional parameters in body. This endpoint can only be called if the bridge implementsanchorstrait
- Domain endpoints
GET /v2/domainsused to resolve wallet domains on the bridge side. This endpoint can only be called if the bridge implementsdomainstrait. See details About Wallets
Bridge traits
Traits defines features implemented by the bridge. It can be informed either when creating a bridge or updating it, and should be sent in the data of bridge payload. It's optional and accepts a list of strings, options are:
credits: bridge implements credits operations. Called in two phase commit process when receiving money.debits: bridge implements debits operations. Called in two phase commit process when sending money.statuses: bridge implements intent statuses receiver. Called when intent receives an update.anchors: bridge implements anchors handler. Associated with alias directory process.domains: bridge implements domains handler. Associated with alias directory process.events: bridge implements events handler. Events are related to ledger effects i.e balance received. See How to register an effect for details.
If traits is not present in bridge record, all the traits above are enabled by default.
Endpoints enabled for each trait
Bridge traits will let the ledger know that the bridge properly implement some endpoint. If the bridge record “traits” property is set but does not have some trait, the endpoints under that trait won't be called.
For example, if a bridge has ["debits"] as the traits property, it will be called over for debits but not for credits. For credits only default ledger validations (Like balances and permissions) will be executed, without calling prepare or commit on the bridge.
| Endpoint | Description | Trait |
|---|---|---|
| POST /credits | Credit prepare | credits |
| POST /credits/:handle/commit | Credit commit | credits |
| POST /credits/:handle/abort | Credit abort | credits |
| POST /debits | Debit prepare | debits |
| POST /debits/:handle/commit | Debit commit | debits |
| POST /debits/:handle/abort | Debit abort | debits |
| PUT /intents/:handle | Intent status update | statuses |
| GET /wallets/:handle/anchors | Anchors query | anchors |
| POST /wallets/:handle/anchors/!lookup | Anchors lookup | anchors |
| GET /wallets/:handle/domains | Domains query | domains |
| POST /effects/:handle | Effect signal triggered | effects |
Filtering traits
Sometimes you don’t want to participate in the Two Phase Commit process for all intents, or sometimes you don’t want to receive intent updates for all intents. For these scenarios, you can specify traits by passing an object with properties method and filter instead of simply the method.
Filters are available to all traits except for domains, because they are used for a GET request with no body, and effects, because you can filter each effect’s payload when creating the effect record.
The filter targets the data property of the event’s payload, see the “Two phase commit endpoints” and “Other traits endpoints” for example payloads of different requests.
For example, if we want our bridge to participate in all credits, in debits when the intent schema is not settle and receive intents status updates for intents which has any claim with an amount greater than or equal to 100, we can define our bridge like this:
Filters for credits and debits are only applied during prepare phase
{
"handle": "bank1",
"config": {
"server": "https://example.com/v2"
},
"traits": [
'credits',
{
"method": 'debits',
"filter": {
"intent.data.schema": {
"$ne": "settle" // No equal settle
}
}
},
{
"method": 'statuses',
"filter": {
"claims.amount": {
"$gte": 100 // Greater than or equal to 100
}
}
}
],
//... Rest of your bridge data
}On filters, you should target the property you want to filter using dot notation. Mongo-like operators are supported.
Claim grouping
By default, when an intent with N claims is created, for each claim where a bridge is the configured bridge of the resolved source or target wallet, the bridge will receive an entry like prepare (debit or credit), commit or abort as described in the following chapter Two phase commit endpoints.
For intents with several claims, a bridge might receive numerous unordered notifications. This can complicate processing.
Optionally, a bridge can be configured so that the ledger groups its calls to the bridge.
To do so, set the bridge's configuration under debits.claims.groupBy or credits.claims.groupBy to wallet or address , so a config might look like this:
Example bridge config
{
"data": {
"handle": "my-bridge",
"config": {
"server": "https://my-server.com/api",
// debits will be consolidated into one call by `source.handle`
"debits.claims.groupBy": "address",
// credits will be consolidated into one call
// by resolved target wallet's `handle`
"credits.claims.groupBy": "wallet"
}
}
}There are two types of grouping:
- grouping by
address- Grouped by the handle set as source/target. For example, for two claims in a
sendintent wheresourcefor both claims ismy-walletbuttargetis different for both claims, the source bridge will receive only one groupedprepareinstead of two single ones, while target bridge will still receive two singlepreparecalls.
- Grouped by the handle set as source/target. For example, for two claims in a
- grouping by
wallet- Grouped by the handle of the resolved source/target wallet. For example, for two claims in a
sendintent wheresourcefor one ismy-walletand the other isother-wallet, but where both wallets get resolved to their shared parentparent-wallet, the source bridge will receive only one groupedprepareinstead of two single ones
- Grouped by the handle of the resolved source/target wallet. For example, for two claims in a
Retries for requests from ledger to bridge
When ledger communicates with a bridge, it will use retry behavior in case the bridge fails to respond with a successful status code. These requests may include operations such
as prepare, commit, abort, status, or delivering a webhook for an effect.
The default settings for this retry mechanism are as follows unless specified differently in the documentation for a specific bridge entry. These are configurable server-wide through environment variables, individually for each type of retry (prepare, commit, abort, status, effect webhook delivery).
- Initial Delay: The initial delay between the first request and the first retry is set to 1 second.
- Backoff Coefficient: Each subsequent retry will occur after a delay that is 20% longer than the previous one.
- Maximum Delay: The delay between retries will not exceed 1 hour.
The maximum delay means that once the delay between retries reaches 1 hour, all subsequent retries will continue to occur every hour until a successful response is received or the operation is otherwise terminated.
In extreme cases, the ledger may employ other criteria for terminating the retry sequence, such as a maximum number of retries or total timeout, as defined in the operational policies or specific bridge documentation. The ledger server supports both global as well as event type specific settings for maximum retries.
The default setting is retrying 5 times before giving up.
When a bridge is back online after being offline for a period of time, it can call the POST /v2/bridges/:handle/activate endpoint which will speed up the events delivery towards that bridge by rescheduling them with no delay.
This will also include deliveries that Ledger gave up on due to Bridge failing too many times and exceeding max retries settings.
That way the bridge does not have to wait up to 1 hour to fully catch up.
Two phase commit endpoints
Following concrete endpoints endpoints will be called by Ledger as part of the two phase commit procedure described here. The Ledger side of the two phase commit interface consists of regular Ledger endpoints called with specific payloads. The endpoints for credit and debit operations are symmetric, the only difference is in the operations that need to be done on the backend.

POST /v2/credits
This endpoint is called when Ledger requests the Bridge to prepare a credit entry. The Bridge should save the entry, respond with 202 Accepted and then process the request.
Example body
{
"data": {
"handle": "cre_baC0LUTVW9lzYA284",
"schema": "credit",
"symbol": {
"handle": "usd"
},
"target": {
"handle": "tel:+57123456789"
},
"amount": 1,
"intent": {
"hash": "05fabc86ce2ef2b6ec2e3cce3fe2257a91f8b8f98023b0821a13702600413232",
"data": {
"handle": "jqgxqSoFB4xl9hXiR",
"claims": [
{
"action": "transfer",
"amount": 1,
"source": "account:1234@bank1.com",
"symbol": "usd",
"target": "tel:+57123456789"
}
],
"access": [ ... ]
},
"meta": {
"status": "pending",
"thread": "YJxAMz52rMPiNYOZV",
"proofs": [ ... ]
}
}
},
"hash": "05fbeaff98e5afed194ee777914b44740ebcb64f38024cd3b43fb1b790ba639c",
"meta": {
"proofs": [
{
"method": "ed25519-v2",
"public": "zUkKMByFGfe7UycJadbGsiLjJq/inu5rCoRvxqGzSQs=",
"digest": "05fbeaff98e5afed194ee777914b44740ebcb64f38024cd3b43fb1b790ba639c",
"result": "MaOcV8W+MD1Gt/l7QLNyqYd/pwV+wS6kcyci8HN/CX3uf63ikugV0K259zG24PFeFSxe31q9JY2/rgqbtdSADQ=="
}
]
}
}POST /v2/credits/:handle/commit
This endpoint is called when Ledger requests the Bridge to commit previously prepared credit entry. The Bridge should load previously saved entry, respond with 202 Accepted and then process the request.
Example body
{
"data": {
"handle": "cre_baC0LUTVW9lzYA284",
"action": "commit",
"intent": {
"hash": "05fabc86ce2ef2b6ec2e3cce3fe2257a91f8b8f98023b0821a13702600413232",
"data": {
"handle": "jqgxqSoFB4xl9hXiR",
"claims": [
{
"action": "transfer",
"amount": 1,
"source": "account:1234@bank1.com",
"symbol": "usd",
"target": "tel:+57123456789"
}
],
"access": [ ... ]
},
"meta": {
"status": "committed",
"thread": "YJxAMz52rMPiNYOZV",
"proofs": [ ... ]
}
}
},
"hash": "60f4e6f6d22dc20f26a4961de137230cdf0f40174f31bf24eee65944176f264b",
"meta": {
"proofs": [
{
"method": "ed25519-v2",
"public": "zUkKMByFGfe7UycJadbGsiLjJq/inu5rCoRvxqGzSQs=",
"digest": "60f4e6f6d22dc20f26a4961de137230cdf0f40174f31bf24eee65944176f264b",
"result": "5ltdzNGQp/7p/xzgznlp+SZ5QzOX81Fa4o/ySOK3Er+bNPT8cGgek+M0bP2cb6aRJA2ptVfbwO9a91l4UKlMBA=="
}
]
}
}POST /v2/credits/:handle/abort
This endpoint is called when Ledger requests the Bridge to abort previously prepared credit entry. The Bridge should load previously saved entry, respond with 202 Accepted and then process the request.
Example body
{
"data": {
"handle": "cre_aLo0Els1vsq7AIQFw",
"action": "abort"
"intent": {
"hash": "05fabc86ce2ef2b6ec2e3cce3fe2257a91f8b8f98023b0821a13702600413232",
"data": {
"handle": "jqgxqSoFB4xl9hXiR",
"claims": [
{
"action": "transfer",
"amount": 1,
"source": "account:1234@bank1.com",
"symbol": "usd",
"target": "tel:+57123456789"
}
],
"access": [ ... ]
},
"meta": {
"status": "aborted",
"thread": "YJxAMz52rMPiNYOZV",
"proofs": [ ... ]
}
}
},
"hash": "d6d189f21690adb852e870404b3cfd074a804789972b4372d2724b5d2d502c5e",
"meta": {
"proofs": [
{
"method": "ed25519-v2",
"public": "zUkKMByFGfe7UycJadbGsiLjJq/inu5rCoRvxqGzSQs=",
"digest": "d6d189f21690adb852e870404b3cfd074a804789972b4372d2724b5d2d502c5e",
"result": "ArX3Jbu2M79EsRlDC3FvjzfGInRZgVmRtd0HRgrQyLLsJf2KKtOneLAeZXR9h+mbPnBELRMRpvuvpGrBoXmqCw=="
}
]
}
}POST /v2/debits
This endpoint is called when Ledger requests the Bridge to prepare a debit entry. The Bridge should save the entry, respond with 202 Accepted and then process the request. The request is analogous to POST /credits.
POST /v2/debits/:handle/commit
This endpoint is called when Ledger requests the Bridge to commit previously prepared debit entry. The Bridge should load previously saved entry, respond with 202 Accepted and then process the request. The request is analogous to POST /debits/:handle/commit.
POST /v2/debits/:handle/abort
This endpoint is called when Ledger requests the Bridge to abort previously prepared debit entry. The Bridge should load previously saved entry, respond with 202 Accepted and then process the request. The request is analogous to POST /debits/:handle/abort.
PUT /v2/intents/:handle
This endpoint is used by the Ledger to report intent status changes to the Bridge. It will be called with an Intent record and the Bridge should insert it into the database if it does not already exist or update it with the new value if it does. This can be used to follow the status of Intents as they are processed, for example when an Intent is fully processed, it will end up in either completed or rejected status.
Example body
{
"hash": "05fabc86ce2ef2b6ec2e3cce3fe2257a91f8b8f98023b0821a13702600413232",
"data": {
"handle": "jqgxqSoFB4xl9hXiR",
"claims": [
{
"action": "transfer",
"amount": 1,
"source": "account:1234@bank1.com",
"symbol": "usd",
"target": "tel:+57123456789"
}
],
"access": [
{
"action": "any",
"signer": {
"public": "GoI6EQLb9Ldw1+2WZwGHVADh1oK0+D+cW9daaQdr1S8="
}
},
{
"action": "read",
"bearer": {
"sub": "GoI6EQLb9Ldw1+2WZwGHVADh1oK0+D+cW9daaQdr1S8="
}
}
]
},
"meta": {
"status": "completed",
"thread": "YJxAMz52rMPiNYOZV",
"proofs": [
{
"digest": "05fabc86ce2ef2b6ec2e3cce3fe2257a91f8b8f98023b0821a13702600413232",
"public": "GoI6EQLb9Ldw1+2WZwGHVADh1oK0+D+cW9daaQdr1S8=",
"result": "J05sbIXg/1DV00bQqJ4RyzTqb2gTTGQz0NawPKpnMw+aEy45fGPCVCauEs8XSIRh6O9R2D9ebNJjvt6DLPQNDQ==",
"method": "ed25519-v2",
"custom": {
"status": "completed",
"moment": "2023-03-09T18:55:39.032Z"
}
}
]
}
}Other traits endpoints
GET /v2/wallets/:handle/anchors
This endpoint is called when anchors are queried for a wallet that has a bridge with trait anchors linked. Since it’s a GET request, it has no payload.
It should return a signed body with valid anchors
Example expected response
{
"hash": "05fabc86ce2ef2b6ec2e3cce3fe2257a91f8b8f98023b0821a13702600413232",
"data": [
{
"handle": "anc-anchor",
"wallet": "anc-wallet",
"target": "anc-target",
// symbol, custom, schema and access properties are supported as well
}
],
"meta": {
"proofs": [
{
"digest": "05fabc86ce2ef2b6ec2e3cce3fe2257a91f8b8f98023b0821a13702600413232",
"public": "GoI6EQLb9Ldw1+2WZwGHVADh1oK0+D+cW9daaQdr1S8=",
"result": "J05sbIXg/1DV00bQqJ4RyzTqb2gTTGQz0NawPKpnMw+aEy45fGPCVCauEs8XSIRh6O9R2D9ebNJjvt6DLPQNDQ==",
"method": "ed25519-v2",
"custom": {
"moment": "2023-03-09T18:55:39.032Z"
}
}
]
}
}POST /v2/wallets/:handle/!lookup
This endpoint is called when anchors are looked up for a wallet that has a bridge with trait anchors linked. The expected response is the same as the one for the endpoint GET /v2/wallets/:handle/anchors, the difference is that the named query !lookup allows payload to be sent.
Example body
{
"hash": "05fabc86ce2ef2b6ec2e3cce3fe2257a91f8b8f98023b0821a13702600413232",
"data": {
"handle": "anc-handle",
// Can contain any property available in LedgerAnchor
},
"meta": {
"proofs": [
{
"digest": "05fabc86ce2ef2b6ec2e3cce3fe2257a91f8b8f98023b0821a13702600413232",
"public": "GoI6EQLb9Ldw1+2WZwGHVADh1oK0+D+cW9daaQdr1S8=",
"result": "J05sbIXg/1DV00bQqJ4RyzTqb2gTTGQz0NawPKpnMw+aEy45fGPCVCauEs8XSIRh6O9R2D9ebNJjvt6DLPQNDQ==",
"method": "ed25519-v2",
"custom": {
"moment": "2023-03-09T18:55:39.032Z"
}
}
]
}
}GET /v2/wallets/:domain
This endpoint is called when domains are queried for a wallet that has a bridge with trait domains linked. Since it’s a GET request, it has no payload.
It should return a signed body with valid domains.
Example expected response
{
"hash": "05fabc86ce2ef2b6ec2e3cce3fe2257a91f8b8f98023b0821a13702600413232",
"data": [
{
"handle": "bank1",
// custom, schema and access properties are supported as well
}
],
"meta": {
"proofs": [
{
"digest": "05fabc86ce2ef2b6ec2e3cce3fe2257a91f8b8f98023b0821a13702600413232",
"public": "GoI6EQLb9Ldw1+2WZwGHVADh1oK0+D+cW9daaQdr1S8=",
"result": "J05sbIXg/1DV00bQqJ4RyzTqb2gTTGQz0NawPKpnMw+aEy45fGPCVCauEs8XSIRh6O9R2D9ebNJjvt6DLPQNDQ==",
"method": "ed25519-v2",
"custom": {
"moment": "2023-03-09T18:55:39.032Z"
}
}
]
}
}Bridge authentication
Here we describe currently supported configurable types of authentication for requests from Ledger to Bridge.
Multiple security rules
Bridges can have multiple security rules configured in their secure array. These rules are applied sequentially in the order they appear in the configuration. Each rule will set a HTTP header on outgoing requests to the bridge, regardless if it overwrites an existing header.
Important: If multiple rules set the same header key, the last rule processed will overwrite any previous values for that header. This allows for flexible authentication strategies but requires careful consideration of rule ordering.
For example, a bridge might use both header-based authentication for API keys and OAuth2 for authorization tokens:
{
"handle": "bank1",
"config": {
"server": "https://example.com/v2"
},
"secure": [
{
"schema": "header",
"key": "X-API-Key",
"value": "{{ secret.apiKey }}"
},
{
"schema": "oauth2",
"clientId": "my-client-id",
"clientSecret": "{{ secret.clientSecret }}",
"tokenUrl": "https://auth.example.com/token"
}
]
}In this example, both the X-API-Key header and the Authorization header (from OAuth2) will be included in requests to the bridge.
Header-based authentication
Header-based authentication allows you to configure custom HTTP headers that will be included in all requests from the Ledger to the Bridge. This is useful for API keys, custom authentication tokens, or any other header-based authentication mechanism.
A header security rule requires:
schema: Must be set to"header"key: The HTTP header name (e.g.,"X-API-Key","Authorization")value: The header value, which should be a secret reference for sensitive data
The header value should use secret references to ensure sensitive information is encrypted. The Ledger will resolve these references at request time using its secret store.
Example header authentication configuration
{
"handle": "payment-processor",
"config": {
"server": "https://api.payment-processor.com/v1"
},
"secure": [
{
"schema": "header",
"key": "X-API-Key",
"value": "{{ secret.processorApiKey }}"
},
{
"schema": "header",
"key": "X-Client-ID",
"value": "{{ secret.clientIdentifier }}"
}
]
}This configuration will add two headers to every request:
X-API-Key: [resolved value of processorApiKey secret]X-Client-ID: [resolved value of clientIdentifier secret]
Header authentication considerations
- Case sensitivity: Header keys are case-sensitive and will be sent exactly as configured
- Secret resolution: Values using secret references (e.g.,
{{ secret.myKey }}) will be resolved from the bridge's encrypted secret store - Header conflicts: If multiple header rules use the same key, the last rule processed will overwrite previous values
- Standard headers: You can set almost any HTTP header with the exception of some security-sensitive headers. However, setting a header like
Content-Typeis likely to break requests to the bridge or be overwritten by the ledger.
Oauth2
As mentioned previously, Oauth2 can be configured in the bridge's secure property. An oauth2 definition requires
a token endpoint that returns an access (not refresh) token, client id and client secret. The given client secret should be a secret reference, where the Ledger
will save an encrypted value instead of the plain value.
The OAuth2 authentication will automatically add an Authorization header with the format Bearer [token] to all requests.
The given token endpoint should receive a Basic header with base64 encoded value of format clientId:clientSecret, and return in its body:
access_token- String value of token that may or may not be a JWT. The token may or may not be a JWT.expires_in- Optional. Remaining expiration time in seconds.
The bridge must respect the value it returns for token expiration time. The value is resolved according to these descending priorities:
- If a token is a JWT and contains an expiration claim (
exp), value ofexpwill be used. - If a token is not a JWT or is a JWT that does not contain an expiration claim, but body defines
expires_in, the body propertyexpires_inwill be used. - If a token is a JWT, does not have
exp, and body does not defineexpires_in, it will be treated like it never expires. - If a token is not a JWT, and the body does not define
expires_in, token will be used only once and never be cached, because we must assume it could expire at any time.
Note: If a bridge returns tokens shorter lived than 60 seconds, they will not be cached by Ledger and will be fetched before every request to Bridge.
OAuth2 and header authentication together
When using both OAuth2 and header authentication, be aware that:
- If a header rule sets the
Authorizationheader, OAuth2 will overwrite it since OAuth2 rules are typically processed after header rules - To avoid conflicts, use header authentication for non-Authorization headers when combining with OAuth2
- The processing order follows the order of rules in the
securearray
Example combined authentication
{
"handle": "bank-integration",
"config": {
"server": "https://bank-api.example.com/v2"
},
"secure": [
{
"schema": "header",
"key": "X-Institution-ID",
"value": "{{ secret.institutionId }}"
},
{
"schema": "header",
"key": "X-Request-ID",
"value": "ledger-bridge-request"
},
{
"schema": "oauth2",
"clientId": "bank-integration-client",
"clientSecret": "{{ secret.oauthSecret }}",
"tokenUrl": "https://auth.bank.example.com/oauth/token"
}
]
}This configuration will result in requests with:
X-Institution-ID: [resolved institutionId secret]X-Request-ID: ledger-bridge-requestAuthorization: Bearer [OAuth2 token]