Create an outgoing payment grant request
The Grant Request API lets you request a grant for outgoing payment, incoming payment, and quote resources.
Before your client can call most of the Open Payments APIs, it must receive a grant from the appropriate authorization server.
The code snippets below let an authenticated client request a grant for an outgoing payment. The request to the authorization server must indicate the outgoing-payment and the actions the client wants to take at the resource server.
Interactive grants
Section titled “Interactive grants”Outgoing payments require explicit consent, typically by the client’s user, before a grant can be issued. Consent is obtained through an interactive grant.
Any authorization server that issues interactive grants must integrate with an identity provider (IdP). When a client requests the outgoing payment grant, the authorization server provides the client with the IdP URI to redirect to.
Before you begin
Section titled “Before you begin”We recommend creating a wallet account on the test wallet. Creating an account allows you to test your client against the Open Payments APIs by using an ILP-enabled wallet funded with play money.
Request an outgoing payment grant
Section titled “Request an outgoing payment grant”Initial configuration
If you’re using JavaScript, only do the first step.
-
Add
"type": "module"topackage.json. -
Add the following to
tsconfig.json{"compilerOptions": {"target": "ES2022","module": "ES2022"}}
Generate without an interval
Section titled “Generate without an interval”// Import dependenciesimport { createAuthenticatedClient } from '@interledger/open-payments'
// Initialize clientconst client = await createAuthenticatedClient({ walletAddressUrl: WALLET_ADDRESS, privateKey: PRIVATE_KEY_PATH, keyId: KEY_ID})
// Get wallet address informationconst walletAddress = await client.walletAddress.get({ url: WALLET_ADDRESS})
// Request outgoing payment grantconst grant = await client.grant.request( { url: walletAddress.authServer }, { access_token: { access: [ { identifier: walletAddress.id, type: 'outgoing-payment', actions: ['list', 'list-all', 'read', 'read-all', 'create'], limits: { debitAmount: { assetCode: quote.debitAmount.assetCode, assetScale: quote.debitAmount.assetScale, value: quote.debitAmount.value } } } ] }, interact: { start: ['redirect'], finish: { method: 'redirect', uri: 'http://localhost:3344', nonce: NONCE } } })
// Check grant stateif (!isPendingGrant(grant)) { throw new Error('Expected interactive grant')}
// Outputconsole.log('Please interact at the following URL:', grant.interact.redirect)console.log('CONTINUE_ACCESS_TOKEN =', grant.continue.access_token.value)console.log('CONTINUE_URI =', grant.continue.uri)Generate with an interval
Section titled “Generate with an interval”// Import dependenciesimport { createAuthenticatedClient } from '@interledger/open-payments'
// Initialize clientconst client = await createAuthenticatedClient({ walletAddressUrl: WALLET_ADDRESS, privateKey: PRIVATE_KEY_PATH, keyId: KEY_ID})
// Get wallet address informationconst walletAddress = await client.walletAddress.get({ url: WALLET_ADDRESS})
// Request outgoing payment grantconst grant = await client.grant.request( { url: walletAddress.authServer }, { access_token: { access: [ { identifier: walletAddress.id, type: 'outgoing-payment', actions: ['list', 'list-all', 'read', 'read-all', 'create'], limits: { debitAmount: { assetCode: quote.debitAmount.assetCode, assetScale: quote.debitAmount.assetScale, value: quote.debitAmount.value }, interval: 'R/2016-08-24T08:00:00Z/P1D' } } ] }, interact: { start: ['redirect'], finish: { method: 'redirect', uri: 'http://localhost:3344', nonce: NONCE } } })
// Check grant stateif (!isPendingGrant(grant)) { throw new Error('Expected interactive grant')}
// Outputconsole.log('Please interact at the following URL:', grant.interact.redirect)console.log('CONTINUE_ACCESS_TOKEN =', grant.continue.access_token.value)console.log('CONTINUE_URI =', grant.continue.uri)For TypeScript, run tsx path/to/directory/index.ts. View full TS source
For JavaScript, run node path/to/directory/index.js. View full JS source
// Import dependenciesuse open_payments::client::api::UnauthenticatedResources;use open_payments::client::AuthenticatedResources;use open_payments::snippets::utils::{create_authenticated_client, get_env_var, load_env};use open_payments::types::{ auth::{ AccessItem, AccessTokenRequest, GrantRequest, InteractFinish, InteractRequest, LimitsOutgoing, OutgoingPaymentAction, }, GrantResponse,};use uuid::Uuid;
// Initialize client// Authenticated client can be also used for unauthenticated resourceslet client = create_authenticated_client()?;
// Get wallet address informationlet wallet_address_url = get_env_var("WALLET_ADDRESS_URL")?;let wallet_address = client.wallet_address().get(&wallet_address_url).await?;
// Request outgoing payment grantlet quote_url = get_env_var("QUOTE_URL")?;let access_token = get_env_var("QUOTE_ACCESS_TOKEN")?;let quote = client.quotes().get("e_url, Some(&access_token)).await?;
let wallet_id = &wallet_address.id;let grant_request = GrantRequest::new( AccessTokenRequest { access: vec![AccessItem::OutgoingPayment { actions: vec![ OutgoingPaymentAction::Read, OutgoingPaymentAction::ReadAll, OutgoingPaymentAction::List, OutgoingPaymentAction::Create, ], identifier: wallet_id.to_string(), limits: Some(LimitsOutgoing { receiver: None, debit_amount: Some(quote.debit_amount), receive_amount: None, interval: None, }), }], }, Some(InteractRequest { start: vec!["redirect".to_string()], finish: Some(InteractFinish { method: "redirect".to_string(), uri: "http://localhost".to_string(), nonce: Uuid::new_v4().to_string(), }), }),);
println!( "Grant request JSON: {}", serde_json::to_string_pretty(&grant_request)?);
let response = client .grant() .request(&wallet_address.auth_server, &grant_request) .await?;
// Outputmatch response { GrantResponse::WithToken { access_token, .. } => { println!("Received access token: {:#?}", access_token.value); println!( "Received access token manage URL: {:#?}", access_token.manage ); } GrantResponse::WithInteraction { interact, continue_, } => { println!("Received interact: {interact:#?}"); println!("Received continue: {continue_:#?}"); }}Generate without an interval
Section titled “Generate without an interval”// Import dependenciesuse OpenPayments\AuthClient;use OpenPayments\Config\Config;
// Initialize client$config = new Config( $WALLET_ADDRESS, $PRIVATE_KEY, $KEY_ID);$opClient = new AuthClient($config);
// Get wallet address information$wallet = $opClient->walletAddress()->get([ 'url' => $config->getWalletAddressUrl()]);
// Request outgoing payment grant$grant = $opClient->grant()->request( [ 'url' => $wallet->authServer ], [ 'access_token' => [ 'access' => [ [ 'type' => 'outgoing-payment', 'actions' => ['list', 'list-all', 'read', 'read-all', 'create'], 'identifier' => $wallet->id, 'limits' => [ 'receiver' => $INCOMING_PAYMENT_URL, //optional 'debitAmount' => [ 'assetCode' => 'USD', 'assetScale' => 2, 'value' => "130", ] ], ] ] ], 'client' => $config->getWalletAddressUrl(), 'interact' => [ 'start' => ["redirect"], 'finish' => [ 'method' => "redirect", 'uri' => 'https://localhost/?paymentId=123423', 'nonce' => "1234567890", ], ] ]);
// Check grant stateif (!$grant instanceof \OpenPayments\Models\PendingGrant) { throw new \Error('Expected interactive grant');}
// Outputecho 'Please interact at the following URL: ' . $grant->interact->redirect . PHP_EOL;echo 'CONTINUE_ACCESS_TOKEN = ' . $grant->continue->access_token->value . PHP_EOL;echo 'CONTINUE_URI = ' . $grant->continue->uri . PHP_EOL;echo 'GRANT OBJECT: ' . PHP_EOL . print_r($grant, true);Generate with an interval and output
Section titled “Generate with an interval and output”// Import dependenciesuse OpenPayments\AuthClient;use OpenPayments\Config\Config;
// Initialize client$config = new Config( $WALLET_ADDRESS, $PRIVATE_KEY, $KEY_ID);$opClient = new AuthClient($config);
// Get wallet address information$wallet = $opClient->walletAddress()->get([ 'url' => $config->getWalletAddressUrl()]);
// Request outgoing payment grant$grant = $opClient->grant()->request( [ 'url' => $wallet->authServer ], [ 'access_token' => [ 'access' => [ [ 'type' => 'outgoing-payment', 'actions' => ['list', 'list-all', 'read', 'read-all', 'create'], 'identifier' => $wallet->id, 'limits' => [ 'debitAmount' => [ 'assetCode' => 'USD', 'assetScale' => 2, 'value' => "132", ], 'interval' => 'R/2025-04-22T08:00:00Z/P1D', ], ] ] ], 'client' => $config->getWalletAddressUrl(), 'interact' => [ 'start' => ["redirect"], 'finish' => [ 'method' => "redirect", 'uri' => 'https://localhost/?paymentId=123423', 'nonce' => "1234567890", ], ] ]);
// Check grant stateif (!$grant instanceof \OpenPayments\Models\PendingGrant) { throw new \Error('Expected interactive grant');}
// Outputecho 'Please interact at the following URL: ' . $grant->interact->redirect . PHP_EOL;echo 'CONTINUE_ACCESS_TOKEN = ' . $grant->continue->access_token->value . PHP_EOL;echo 'CONTINUE_URI = ' . $grant->continue->uri . PHP_EOL;echo 'GRANT OBJECT: ' . PHP_EOL . print_r($grant, true);package main
// Import dependenciesimport ( "context" "encoding/json" "fmt" "log"
op "github.com/interledger/open-payments-go" as "github.com/interledger/open-payments-go/generated/authserver")
func main() { // Initialize client client, err := op.NewAuthenticatedClient(WALLET_ADDRESS_URL, PRIVATE_KEY_BASE_64, KEY_ID) if err != nil { log.Fatalf("Error creating authenticated client: %v\n", err) }
// Get wallet address information walletAddress, err := client.WalletAddress.Get(context.TODO(), op.WalletAddressGetParams{ URL: WALLET_ADDRESS_URL, }) if err != nil { log.Fatalf("Error fetching wallet address: %v\n", err) }
// Request outgoing payment grant outgoingAccess := as.AccessOutgoing{ Type: as.OutgoingPayment, Actions: []as.AccessOutgoingActions{ as.AccessOutgoingActionsCreate, as.AccessOutgoingActionsRead, as.AccessOutgoingActionsList, }, Identifier: *walletAddress.Id, } accessItem := as.AccessItem{} if err := accessItem.FromAccessOutgoing(outgoingAccess); err != nil { log.Fatalf("Error creating AccessItem: %v\n", err) } accessToken := struct { Access as.Access `json:"access"` }{ Access: []as.AccessItem{accessItem}, } interact := &as.InteractRequest{ Start: []as.InteractRequestStart{as.InteractRequestStartRedirect}, }
grant, err := client.Grant.Request(context.TODO(), op.GrantRequestParams{ URL: *walletAddress.AuthServer, RequestBody: as.GrantRequestWithAccessToken{ AccessToken: accessToken, Interact: interact, }, }) if err != nil { log.Fatalf("Error requesting grant: %v\n", err) }
// Check grant state if !grant.IsInteractive() { log.Fatalf("Expected interactive grant") }
// Output grantJSON, err := json.MarshalIndent(grant, "", " ") if err != nil { log.Fatalf("Error marshaling grant: %v\n", err) } fmt.Println("GRANT:", string(grantJSON)) fmt.Println("Please interact at the following URL:", grant.Interact.Redirect) fmt.Println("CONTINUE_ACCESS_TOKEN =", grant.Continue.AccessToken.Value) fmt.Println("CONTINUE_URI =", grant.Continue.Uri)}// Import dependenciesimport org.interledger.openpayments.httpclient.OpenPaymentsHttpClient;import org.interledger.openpayments.IOpenPaymentsClient;
// Initialize clientvar client = OpenPaymentsHttpClient.defaultClient("WalletAddress","PrivateKeyPEM","KeyId");
// Get wallet address informationvar senderWallet = client.walletAddress().get("https://cloudninebank.example.com/customer");var receiverWallet = client.walletAddress().get("https://cloudninebank.example.com/merchant");
// Create an incoming paymentvar grantRequest = client.auth().grant().incomingPayment(receiverWallet);var incomingPayment = client.payment().createIncoming(receiverWallet, grantRequest, BigDecimal.valueOf(11.25));
// Create a quotevar quoteRequest = client.auth().grant().quote(senderWallet);var quote = client.quote().create(quoteRequest.getAccess().getToken(), senderWallet, incomingPayment, Optional.empty(), Optional.empty());
var urlToOpen = "https://example.com/redirect?paymentId=1234";
// Create an outgoing payment from quotevar opContinueInteract = client.auth().grant().continuation(senderWallet,quote.getDebitAmount(),URI.create(urlToOpen),"test");
// Outputlog.info("OUTGOING_PAYMENT_GRANT: {}", opContinueInteract);Generate without an interval
Section titled “Generate without an interval”// Import dependenciesusing Microsoft.Extensions.DependencyInjection;using Newtonsoft.Json;using OpenPayments.Sdk.Clients;using OpenPayments.Sdk.Extensions;using OpenPayments.Sdk.Generated.Auth;using OpenPayments.Sdk.Generated.Resource;using OpenPayments.Sdk.HttpSignatureUtils;using Amount = OpenPayments.Sdk.Generated.Auth.Amount;
// Initialize clientvar client = new ServiceCollection() .UseOpenPayments(opts => { opts.UseAuthenticatedClient = true; opts.KeyId = CLIENT_ID; opts.PrivateKey = KeyUtils.LoadPem(CLIENT_SECRET); opts.ClientUrl = new Uri(CLIENT_WALLET_ADDRESS); }) .BuildServiceProvider() .GetRequiredService<IAuthenticatedClient>();
// Get wallet address informationvar walletAddress = await client.GetWalletAddressAsync(WALLET_ADDRESS);
// Request quote grantvar grant = await client.RequestGrantAsync( new RequestArgs() { Url = walletAddress.AuthServer }, new GrantCreateBodyWithInteract { AccessToken = new AccessToken { Access = [ new OutgoingAccess { Actions = [Actions.List, Actions.ListAll, Actions.Read, Actions.ReadAll, Actions.Create], Identifier = walletAddress.Id, Limits = new OutgoingAccessLimits { DebitAmount = new Amount(quote.DebitAmount.Value, quote.DebitAmount.AssetCode, quote.DebitAmount.AssetScale), }, }, ], }, Interact = new InteractRequest() { Start = [Start.Redirect], Finish = new Finish() { Method = FinishMethod.Redirect, Uri = new Uri("http://localhost:3344"), Nonce = NONCE } }, });
// Check grant stateif (grant.Interact == null){ throw new Exception("Expected interactive grant");}
// OutputConsole.WriteLine($"Please interact at the following URL: {grant.Interact.Redirect}");Console.WriteLine($"CONTINUE_ACCESS_TOKEN = {grant.Continue.AccessToken.Value}");Console.WriteLine($"CONTINUE_URI = {grant.Continue.Uri}");Generate with an interval
Section titled “Generate with an interval”// Import dependenciesusing Microsoft.Extensions.DependencyInjection;using Newtonsoft.Json;using OpenPayments.Sdk.Clients;using OpenPayments.Sdk.Extensions;using OpenPayments.Sdk.Generated.Auth;using OpenPayments.Sdk.Generated.Resource;using OpenPayments.Sdk.HttpSignatureUtils;using Amount = OpenPayments.Sdk.Generated.Auth.Amount;
// Initialize clientvar client = new ServiceCollection() .UseOpenPayments(opts => { opts.UseAuthenticatedClient = true; opts.KeyId = CLIENT_ID; opts.PrivateKey = KeyUtils.LoadPem(CLIENT_SECRET); opts.ClientUrl = new Uri(CLIENT_WALLET_ADDRESS); }) .BuildServiceProvider() .GetRequiredService<IAuthenticatedClient>();
// Get wallet address informationvar walletAddress = await client.GetWalletAddressAsync(WALLET_ADDRESS);
// Request quote grantvar grant = await client.RequestGrantAsync( new RequestArgs { Url = walletAddress.AuthServer }, new GrantCreateBodyWithInteract { AccessToken = new AccessToken { Access = [ new OutgoingAccess { Actions = [Actions.List, Actions.ListAll, Actions.Read, Actions.ReadAll, Actions.Create], Identifier = walletAddress.Id, Limits = new OutgoingAccessLimits { DebitAmount = new Amount(quote.DebitAmount.Value, quote.DebitAmount.AssetCode, quote.DebitAmount.AssetScale), Interval = "R/2016-08-24T08:00:00Z/P1D" }, }, ], }, Interact = new InteractRequest { Start = [Start.Redirect], Finish = new Finish { Method = FinishMethod.Redirect, Uri = new Uri("http://localhost:3344"), Nonce = NONCE } }, });
// Check grant stateif (grant.Interact == null){ throw new Exception("Expected interactive grant");}
// OutputConsole.WriteLine($"Please interact at the following URL: {grant.Interact.Redirect}");Console.WriteLine($"CONTINUE_ACCESS_TOKEN = {grant.Continue.AccessToken.Value}");Console.WriteLine($"CONTINUE_URI = {grant.Continue.Uri}");