Skip to main content
This guide covers how to integrate Plaid payments into your app. Before you begin, make sure you’ve configured your Plaid connection in the dashboard. Processing a payment with Plaid involves three steps:
  1. Create a Link token - Generate a Plaid Link token via the API using your configured settings
  2. Render Plaid Link - Display the Plaid Link interface to the customer so they can securely connect their bank account
  3. Create a transaction - Submit the public token received from Plaid Link to process the payment
Looking for a complete working example? Check out this sample standalone Plaid app on GitHub that demonstrates the full integration flow.
Use the payment service session API with the create-link-token action to generate a Plaid Link token. This endpoint applies your dashboard settings (identity, signal, balance checks, etc.) to the token request. You can also override any settings by passing custom Plaid Link parameters such as products, additional_consented_products, and optional_products to customize the experience for specific transactions. Refer to the Plaid Link token API documentation for a complete list of available parameters.
using Gr4vy;
using Gr4vy.Models.Components;
using System.Collections.Generic;

var sdk = new Gr4vySDK(
    id: "example",
    server: SDKConfig.Server.Sandbox,
    bearerAuthSource: Auth.WithToken(privateKey),
    merchantAccountId: "default"
);

// SessionAsync takes the definition ID and a free-form request body
var session = await sdk.PaymentServiceDefinitions.SessionAsync(
    paymentServiceDefinitionId: "plaid-bank",
    requestBody: new Dictionary<string, object>
    {
        ["action"] = "create-link-token",
        ["payload"] = new Dictionary<string, object>
        {
            ["products"] = new[] { "auth" },
            ["optional_products"] = new[] { "signal" }
        }
    }
);

// The downstream Plaid response is passed through in ResponseBody
Console.WriteLine($"Link token: {session.ResponseBody?["link_token"]}");
package main

import (
  "context"
  "log"

  gr4vy "github.com/gr4vy/gr4vy-go"
)

func main() {
  ctx := context.Background()

  // Load your private key from disk, env, or your secret manager
  privateKey := "..."

  s := gr4vy.New(
    gr4vy.WithID("example"),
    gr4vy.WithServer(gr4vy.ServerSandbox),
    gr4vy.WithBearerAuth(gr4vy.WithToken(privateKey)),
    gr4vy.WithMerchantAccountID("default"),
  )

  // Session takes the definition ID and a free-form request body
  res, err := s.PaymentServiceDefinitions.Session(ctx, "plaid-bank", map[string]any{
    "action": "create-link-token",
    "payload": map[string]any{
      "products":          []string{"auth"},
      "optional_products": []string{"signal"},
    },
  })
  if err != nil {
    log.Fatal(err)
  }
  if res != nil {
    // The downstream Plaid response is passed through in ResponseBody
    log.Printf("Link token: %v", res.ResponseBody["link_token"])
  }
}
import com.gr4vy.sdk.Gr4vy;
import com.gr4vy.sdk.BearerSecuritySource;
import com.gr4vy.sdk.Gr4vy.AvailableServers;
import java.util.HashMap;
import java.util.Map;

Gr4vy sdk = Gr4vy.builder()
    .id("example")
    .server(AvailableServers.SANDBOX)
    .merchantAccountId("default")
    .securitySource(new BearerSecuritySource.Builder(privateKey).build())
    .build();

Map<String, Object> payload = new HashMap<>();
payload.put("products", new String[]{"auth"});
payload.put("optional_products", new String[]{"signal"});

Map<String, Object> requestBody = new HashMap<>();
requestBody.put("action", "create-link-token");
requestBody.put("payload", payload);

var res = sdk.paymentServiceDefinitions().session()
    .paymentServiceDefinitionId("plaid-bank")
    .requestBody(requestBody)
    .call();

// The downstream Plaid response is passed through in responseBody
if (res.responseBody().isPresent()) {
    System.out.println("Link token: " + res.responseBody().get().get("link_token"));
}
import { Gr4vy, withToken } from "@gr4vy/sdk";
import fs from "fs";

const gr4vy = new Gr4vy({
  id: "example",
  server: "sandbox",
  bearerAuth: withToken({
    privateKey: fs.readFileSync("private_key.pem", "utf8"),
  }),
});

const session = await gr4vy.paymentServiceDefinitions.session(
  {
    action: "create-link-token",
    payload: {
      // Override default products: require auth, make signal optional
      products: ["auth"],
      optional_products: ["signal"],
    },
  },
  "plaid-bank",
);

// The downstream Plaid response is passed through in responseBody
console.log(session.responseBody?.link_token); // Use this in Plaid Link
declare(strict_types=1);

require 'vendor/autoload.php';

use Gr4vy;
use Gr4vy\Auth;

$sdk = Gr4vy\SDK::builder()
    ->setId('example')
    ->setServer('sandbox')
    ->setSecuritySource(Auth::withToken($privateKey))
    ->setMerchantAccountId('default')
    ->build();

$response = $sdk->paymentServiceDefinitions->session(
    requestBody: [
        'action' => 'create-link-token',
        'payload' => [
            'products' => ['auth'],
            'optional_products' => ['signal']
        ]
    ],
    paymentServiceDefinitionId: 'plaid-bank'
);

// The downstream Plaid response is passed through in responseBody
echo "Link token: " . $response->responseBody['link_token'];
from gr4vy import Gr4vy, auth

with Gr4vy(
    id="example",
    server="sandbox",
    merchant_account_id="default",
    bearer_auth=auth.with_token(open("./private_key.pem").read())
) as g_client:

    res = g_client.payment_service_definitions.session(
        payment_service_definition_id="plaid-bank",
        request_body={
            "action": "create-link-token",
            "payload": {
                "products": ["auth"],
                "optional_products": ["signal"]
            }
        }
    )

    # The downstream Plaid response is passed through in response_body
    print(f"Link token: {res.response_body['link_token']}")
Display Plaid Link using the link_token from the previous step. Plaid Link securely handles bank authentication and account linking for the customer. When the customer successfully links their account, the onSuccess callback receives a publicToken and metadata containing account information. For detailed implementation guides, see the Plaid Link documentation.
import com.plaid.link.Plaid
import com.plaid.link.configuration.LinkTokenConfiguration

class MainActivity : AppCompatActivity() {

  private val linkAccountToPlaid = registerForActivityResult(
    FastOpenPlaidLink()
  ) { result ->
    when (result) {
      is LinkSuccess -> {
        // Send publicToken to your server to create a transaction
        val publicToken = result.publicToken
        val metadata = result.metadata
        submitPayment(publicToken, metadata)
      }
      is LinkExit -> {
        // Handle user exit
        result.error?.let { error ->
          Log.e("Link", "Link exit with error: ${error.errorMessage}")
        }
      }
    }
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Configure Link with your link_token
    val linkTokenConfiguration = LinkTokenConfiguration.Builder()
      .token(linkToken)
      .build()

    // Create and open Link
    val plaidHandler = Plaid.create(application, linkTokenConfiguration)
    linkAccountToPlaid.launch(plaidHandler)
  }
}
import LinkKit

class ViewController: UIViewController {
  private var handler: Handler?

  override func viewDidLoad() {
    super.viewDidLoad()

    // Configure Link with your link_token
    var config = LinkTokenConfiguration(token: linkToken) { success in
      // Send publicToken to your server to create a transaction
      let publicToken = success.publicToken
      let metadata = success.metadata
      self.submitPayment(publicToken: publicToken, metadata: metadata)
    }

    config.onExit = { exit in
      // Handle user exit
      if let error = exit.error {
        print("Link exit with error: \(error)")
      }
    }

    // Create and open Link
    let result = Plaid.create(config)
    switch result {
    case .success(let handler):
      self.handler = handler
      handler.open(presentUsing: .viewController(self))
    case .failure(let error):
      print("Error creating Link: \(error)")
    }
  }
}
import { usePlaidLink } from "react-plaid-link";

export function BankAccountForm({ linkToken }) {
  const { open, ready } = usePlaidLink({
    token: linkToken,
    onSuccess: (publicToken, metadata) => {
      // Send publicToken to your server to create a transaction
      submitPayment(publicToken, metadata);
    },
    onExit: (error, metadata) => {
      // Handle user exit
      if (error) {
        console.error("Link exit with error:", error);
      }
    },
  });

  return (
    <button onClick={() => open()} disabled={!ready}>
      Link your bank account
    </button>
  );
}
<script src="https://cdn.plaid.com/link/v2/stable/link-initialize.js"></script>
<script>
  const handler = Plaid.create({
    token: linkToken,
    onSuccess: (publicToken, metadata) => {
      // Send publicToken to your server to create a transaction
      submitPayment(publicToken, metadata);
    },
    onExit: (error, metadata) => {
      // Handle user exit
      if (error) {
        console.error("Link exit with error:", error);
      }
    },
  });

  // Open Link when user clicks button
  document.getElementById("link-button").onclick = function () {
    handler.open();
  };
</script>

Step 3: Create a transaction

On your server, use the publicToken from Step 2 to create a transaction. The publicToken is automatically exchanged for a secure connection to the customer’s bank account. Key parameters (API field names):
  • payment_service_id (paymentServiceId in SDKs) - The UUID of your bank payment processor connection (for example, Plaid Transfer or Adyen) to process the payment through.
  • payment_method.payment_service_id (paymentMethod.paymentServiceId in SDKs) - Optional. The UUID of the Plaid connection (found in the dashboard under Settings -> Connections -> Plaid). If omitted, the system fails the transaction if multiple plaid-bank connectors have been configured.
  • payment_method.account_id (accountId on the payment method in SDKs) - Required if the identity product is not enabled on the Link token and/or merchant account. If identity is enabled, the account ID is fetched automatically. When provided, specifies which account to charge if the customer linked multiple accounts (available in the Plaid metadata).
  • buyer - The buyer’s first and last name are required for transaction processing and identity matching, unless the identity product is enabled and used to fetch this information automatically from Plaid.
using Gr4vy;
using Gr4vy.Models.Components;

var sdk = new Gr4vySDK(
    id: "example",
    server: SDKConfig.Server.Sandbox,
    bearerAuthSource: Auth.WithToken(privateKey),
    merchantAccountId: "default"
);

var res = await sdk.Transactions.CreateAsync(
    transactionCreate: new TransactionCreate()
    {
        Amount = 1299,
        Currency = "USD",
        Country = "US",
        Intent = "capture",
        
        // PaymentMethod is a discriminated union; use the factory method for Plaid.
        // The method discriminator defaults to "plaid" and isn't set explicitly.
        PaymentMethod = TransactionCreatePaymentMethod.CreatePlaidPaymentMethodCreate(
            new PlaidPaymentMethodCreate()
            {
                Token = publicToken,
                AccountId = metadata.Accounts[0].Id
            }
        ),
        
        Buyer = new BuyerCreate()
        {
            BillingAddress = new AddressCreate()
            {
                FirstName = "John",
                LastName = "Doe"
            }
        },
        
        PaymentServiceId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    }
);

Console.WriteLine($"Status: {res.Status}");
package main

import (
	"context"
	"log"

	gr4vy "github.com/gr4vy/gr4vy-go"
	"github.com/gr4vy/gr4vy-go/models/components"
)

func main() {
	ctx := context.Background()

	// Load your private key from disk, env, or your secret manager
	privateKey := "..."

	s := gr4vy.New(
		gr4vy.WithID("example"),
		gr4vy.WithServer(gr4vy.ServerSandbox),
		gr4vy.WithBearerAuth(gr4vy.WithToken(privateKey)),
		gr4vy.WithMerchantAccountID("default"),
	)

	// PaymentMethod is a discriminated union; wrap PlaidPaymentMethodCreate.
	// The method discriminator defaults to "plaid" and isn't set explicitly.
	plaidPaymentMethod := components.PlaidPaymentMethodCreate{
		Token:     publicToken,                              // Public token received from Plaid Link
		AccountID: gr4vy.String(metadata.Accounts[0].ID),    // Optional. Specify which account to charge
	}
	paymentMethod := components.CreateTransactionCreatePaymentMethodPlaidPaymentMethodCreate(plaidPaymentMethod)

	res, err := s.Transactions.Create(ctx, components.TransactionCreate{
		Amount:        1299,
		Currency:      "USD",
		Country:       gr4vy.String("US"),
		Intent:        gr4vy.Pointer(components.TransactionIntentCapture),
		PaymentMethod: &paymentMethod,
		Buyer: &components.BuyerCreate{
			BillingAddress: &components.AddressCreate{
				FirstName: gr4vy.String("John"),
				LastName:  gr4vy.String("Doe"),
			},
		},
		PaymentServiceID: gr4vy.String("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"), // Your bank payment processor connection ID (e.g., Plaid Transfer)
	}, nil, nil, nil)
	if err != nil {
		log.Fatal(err)
	}
	if res != nil {
		log.Printf("Status: %v", res.Status)
	}
}
import com.gr4vy.sdk.Gr4vy;
import com.gr4vy.sdk.BearerSecuritySource;
import com.gr4vy.sdk.Gr4vy.AvailableServers;
import com.gr4vy.sdk.models.components.*;
import java.util.Optional;

Gr4vy sdk = Gr4vy.builder()
    .id("example")
    .server(AvailableServers.SANDBOX)
    .merchantAccountId("default")
    .securitySource(new BearerSecuritySource.Builder(privateKey).build())
    .build();

// PaymentMethod is a discriminated union; wrap PlaidPaymentMethodCreate.
// The method discriminator defaults to "plaid" and isn't set explicitly.
var res = sdk.transactions().create()
    .transactionCreate(TransactionCreate.builder()
        .amount(1299L)
        .currency("USD")
        .country("US")
        .intent(TransactionIntent.CAPTURE)
        .paymentMethod(TransactionCreatePaymentMethod.of(PlaidPaymentMethodCreate.builder()
            .token(publicToken) // Public token received from Plaid Link
            .accountId(metadata.getAccounts().get(0).getId()) // Optional. Specify which account to charge
            .build()))
        .buyer(BuyerCreate.builder()
            .billingAddress(AddressCreate.builder()
                .firstName("John")
                .lastName("Doe")
                .build())
            .build())
        .paymentServiceId("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") // UUID of the bank processor connection (e.g. Plaid Transfer) used to process this transaction
        .build())
    .call();

if (res.transaction().isPresent()) {
    System.out.println("Status: " + res.transaction().get().status());
}
import { Gr4vy, withToken } from "@gr4vy/sdk";
import fs from "fs";

const gr4vy = new Gr4vy({
  id: "example",
  server: "sandbox",
  merchantAccountId: "default",
  bearerAuth: withToken({
    privateKey: fs.readFileSync("private_key.pem", "utf8"),
  }),
});

const response = await gr4vy.transactions.create({
  amount: 1299,
  currency: "USD",
  country: "US",
  intent: "capture",
  paymentMethod: {
    method: "plaid",
    token: publicToken, // Public token received from Plaid Link
    accountId: metadata.accounts[0].id, // Optional. Specify which account to charge
  },
  buyer: {
    billingAddress: {
      firstName: "John",
      lastName: "Doe",
    },
  },
  paymentServiceId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", // Your Plaid connection UUID
});

console.log(`Status: ${response.transaction?.status}`);
declare(strict_types=1);

require 'vendor/autoload.php';

use Gr4vy;
use Gr4vy\Auth;

$sdk = Gr4vy\SDK::builder()
    ->setId('example')
    ->setServer('sandbox')
    ->setSecuritySource(Auth::withToken($privateKey))
    ->setMerchantAccountId('default')
    ->build();

$response = $sdk->transactions->create(
    transactionCreate: new Gr4vy\TransactionCreate(
        amount: 1299,
        currency: 'USD',
        country: 'US',
        intent: 'capture',
        paymentMethod: new Gr4vy\PlaidPaymentMethodCreate(
            token: $publicToken, // Public token received from Plaid Link
            accountId: $metadata['accounts'][0]['id'] // Optional. Specify which account to charge
        ),
        buyer: new Gr4vy\BuyerCreate(
            billingAddress: new Gr4vy\AddressCreate(
                firstName: 'John',
                lastName: 'Doe'
            )
        ),
        paymentServiceId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' // Your Plaid connection UUID
    )
);

echo "Status: " . $response->transaction->status;
from gr4vy import Gr4vy, auth

with Gr4vy(
    id="example",
    server="sandbox",
    merchant_account_id="default",
    bearer_auth=auth.with_token(open("./private_key.pem").read())
) as g_client:

    res = g_client.transactions.create(
        amount=1299,
        currency="USD",
        country="US",
        intent="capture",
        payment_method={
            "method": "plaid",
            "token": public_token,  # Public token received from Plaid Link
            "account_id": metadata["accounts"][0]["id"]  # Optional. Specify which account to charge
        },
        buyer={
            "billing_address": {
                "first_name": "John",
                "last_name": "Doe"
            }
        },
        payment_service_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"  # Your Plaid connection UUID
    )

    print(f"Status: {res.status}")
If Plaid is still fetching identity data and the call times out, the API returns unavailable_payment_method. This means Plaid did not finish in time; retry the transaction to allow identity to complete.

Next steps

To enable recurring payments and vault customer bank accounts for future transactions, see Recurring Payments. If you’re using Plaid Signal, configure your ruleset in Signal setup.