> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gr4vy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Plaid Recurring Payments

> Set up recurring payments with Plaid.

This guide covers how to vault Plaid payment methods for recurring transactions. Before you begin, make sure you've [configured your Plaid connection](/connections/payments/plaid-bank) and understand the [basic integration flow](/connections/payments/plaid-bank-integration).

You can vault a Plaid payment method by setting `store: true` on the first (customer-present) transaction. The system stores the Plaid access token and returns a `payment_method.id`. Use that ID for subsequent merchant-initiated charges without collecting a new Plaid Link token.

<Note>
  Identity and Identity Match checks are performed only on the initial transaction. Balance and Signal checks are performed on both the initial transaction and all subsequent recurring payments, as these checks evaluate transaction-specific risk factors that may change with each payment.
</Note>

If you're using Plaid Signal, configure your ruleset in [Signal setup](/connections/payments/plaid-bank-signal).

## Step 1: First payment

Set `store: true`, mark it as `payment_source: "recurring"`, and leave `merchant_initiated` / `is_subsequent_payment` false.

<CodeGroup>
  ```csharp C# theme={"system"}
  var res = await sdk.Transactions.CreateAsync(
      transactionCreate: new TransactionCreate()
      {
          Amount = 1299,
          Currency = "USD",
          Country = "US",
          Intent = TransactionCreate.IntentEnum.Capture,
          
          PaymentMethod = TransactionCreatePaymentMethod.CreatePlaidPaymentMethodCreate(
              new PlaidPaymentMethodCreate()
              {
                  Method = PlaidPaymentMethodCreate.MethodEnum.Plaid,
                  Token = publicToken
              }
          ),
          
          Store = true,
          PaymentSource = TransactionCreate.PaymentSourceEnum.Recurring,
          MerchantInitiated = false,
          IsSubsequentPayment = false,
          PaymentServiceId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
      }
  );
  ```

  ```go Go theme={"system"}
  store := true
  merchantInitiated := false
  isSubsequent := false

  res, err := s.Transactions.Create(ctx, &components.TransactionCreate{
    Amount:  gr4vy.Int64(1299),
    Currency: "USD",
    Country:  "US",
    Intent:   components.CreateTransactionIntentCapture,
    PaymentMethod: &components.PaymentMethodRequest{
      Method: components.PaymentMethodRequestMethodPlaid,
      Token:  publicToken,
    },
    Store:              &store,
    PaymentSource:      gr4vy.String("recurring"),
    MerchantInitiated:  &merchantInitiated,
    IsSubsequentPayment: &isSubsequent,
    PaymentServiceID:   "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  })
  ```

  ```java Java theme={"system"}
  TransactionCreate transactionCreate = TransactionCreate.builder()
    .amount(1299)
    .currency("USD")
    .country("US")
    .intent(TransactionCreate.IntentEnum.CAPTURE)
    .paymentMethod(PaymentMethodRequest.builder()
      .method("plaid")
      .token(publicToken)
      .build())
    .store(true)
    .paymentSource("recurring")
    .merchantInitiated(false)
    .isSubsequentPayment(false)
    .paymentServiceId("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
    .build();

  var res = sdk.transactions().create()
    .transactionCreate(transactionCreate)
    .call();
  ```

  ```js TypeScript theme={"system"}
  const res = await gr4vy.transactions.create({
    amount: 1299,
    currency: "USD",
    country: "US",
    intent: "capture",
    paymentMethod: {
      method: "plaid",
      token: publicToken,
    },
    store: true,
    paymentSource: "recurring",
    merchantInitiated: false,
    isSubsequentPayment: false,
    paymentServiceId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  });
  ```

  ```php PHP theme={"system"}
  $res = $sdk->transactions->create(
      transactionCreate: new Gr4vy\TransactionCreate(
          amount: 1299,
          currency: 'USD',
          country: 'US',
          intent: 'capture',
          paymentMethod: new Gr4vy\PaymentMethodRequest(
              method: 'plaid',
              token: $publicToken,
          ),
          store: true,
          paymentSource: 'recurring',
          merchantInitiated: false,
          isSubsequentPayment: false,
          paymentServiceId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
      )
  );
  ```

  ```python Python theme={"system"}
  res = g_client.transactions.create(
    transaction_create={
      "amount": 1299,
      "currency": "USD",
      "country": "US",
      "intent": "capture",
      "payment_method": {
        "method": "plaid",
        "token": public_token,
      },
      "store": True,
      "payment_source": "recurring",
      "merchant_initiated": False,
      "is_subsequent_payment": False,
      "payment_service_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    }
  )
  ```
</CodeGroup>

## Step 2: Subsequent charge

Use the saved `payment_method_id`, set `payment_source: "recurring"`, and mark it as merchant-initiated and subsequent.

<CodeGroup>
  ```csharp C# theme={"system"}
  var res = await sdk.Transactions.CreateAsync(
      transactionCreate: new TransactionCreate()
      {
          Amount = 1299,
          Currency = "USD",
          Country = "US",
          Intent = TransactionCreate.IntentEnum.Capture,
          
          // Use the ID factory for stored payment methods
          PaymentMethod = TransactionCreatePaymentMethod.CreateId("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"),
          
          PaymentSource = TransactionCreate.PaymentSourceEnum.Recurring,
          MerchantInitiated = true,
          IsSubsequentPayment = true,
          PaymentServiceId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
      }
  );
  ```

  ```go Go theme={"system"}
  merchantInitiated := true
  isSubsequent := true

  res, err := s.Transactions.Create(ctx, &components.TransactionCreate{
    Amount:  gr4vy.Int64(1299),
    Currency: "USD",
    Country:  "US",
    Intent:   components.CreateTransactionIntentCapture,
    PaymentMethod: &components.PaymentMethodRequest{
      Method: components.PaymentMethodRequestMethodId,
      Id:     gr4vy.String("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"),
    },
    PaymentSource:      gr4vy.String("recurring"),
    MerchantInitiated:  &merchantInitiated,
    IsSubsequentPayment: &isSubsequent,
    PaymentServiceID:   "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  })
  ```

  ```java Java theme={"system"}
  TransactionCreate transactionCreate = TransactionCreate.builder()
    .amount(1299)
    .currency("USD")
    .country("US")
    .intent(TransactionCreate.IntentEnum.CAPTURE)
    .paymentMethod(PaymentMethodRequest.builder()
      .method("id")
      .id("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
      .build())
    .paymentSource("recurring")
    .merchantInitiated(true)
    .isSubsequentPayment(true)
    .paymentServiceId("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
    .build();

  var res = sdk.transactions().create()
    .transactionCreate(transactionCreate)
    .call();
  ```

  ```js TypeScript theme={"system"}
  const res = await gr4vy.transactions.create({
    amount: 1299,
    currency: "USD",
    country: "US",
    intent: "capture",
    paymentMethod: {
      method: "id",
      id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    },
    paymentSource: "recurring",
    merchantInitiated: true,
    isSubsequentPayment: true,
    paymentServiceId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  });
  ```

  ```php PHP theme={"system"}
  $res = $sdk->transactions->create(
      transactionCreate: new Gr4vy\TransactionCreate(
          amount: 1299,
          currency: 'USD',
          country: 'US',
          intent: 'capture',
          paymentMethod: new Gr4vy\PaymentMethodRequest(
              method: 'id',
              id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
          ),
          paymentSource: 'recurring',
          merchantInitiated: true,
          isSubsequentPayment: true,
          paymentServiceId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
      )
  );
  ```

  ```python Python theme={"system"}
  res = g_client.transactions.create(
    transaction_create={
      "amount": 1299,
      "currency": "USD",
      "country": "US",
      "intent": "capture",
      "payment_method": {
        "method": "id",
        "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      },
      "payment_source": "recurring",
      "merchant_initiated": True,
      "is_subsequent_payment": True,
      "payment_service_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    }
  )
  ```
</CodeGroup>

## Re-authenticating stored payment methods

Stored Plaid payment methods occasionally require re-authentication. This is typically needed when bank credentials expire, the bank requires new MFA, consent is nearing expiry, or the connection is in an error state. The main signal is a failed transaction on that stored method with `error_code: payment_method_requires_action`.

You can also detect upcoming or expired mandates via webhooks. The system emits `payment-method.authorization-expiring` (token close to expiring) and `payment-method.authorization-update-required` (token expired) events when receiving updates from Plaid.

When you see this error, create a new Link token for the stored payment method using the [Sessions API](/reference/payment-service-sessions/create-payment-service-session). Include the stored payment method ID in the `x-gr4vy-payment-method` header and use this payload structure, where `access_token` is replaced by the system with the vaulted access token:

<CodeGroup>
  ```csharp C# theme={"system"}
  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"
  );

  var session = await sdk.PaymentServiceDefinitions.SessionAsync(
      paymentServiceDefinitionId: "plaid-bank",
      sessionRequestBody: new SessionRequestBody()
      {
          Action = "create-link-token",
          Payload = new Dictionary<string, object>
          {
              ["access_token"] = "{{PLAID_ACCESS_TOKEN}}"
          }
      },
      // The payment method ID is passed via a specific optional parameter, usually mapped to the header
      xGr4vyPaymentMethod: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  );

  Console.WriteLine($"Link token: {session.LinkToken}");
  ```

  ```go Go theme={"system"}
  package main

  import (
    "context"
    "log"

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

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

    privateKey := "..."

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

    paymentMethodID := "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

    res, err := s.PaymentServiceDefinitions.Session(ctx, operations.SessionRequest{
      Action: "create-link-token",
      PaymentServiceDefinitionID: "plaid-bank",
      XGr4vyPaymentMethod: &paymentMethodID,
    }, &operations.WithRequestBody{
      Payload: map[string]interface{}{
        "access_token": "{{PLAID_ACCESS_TOKEN}}",
      },
    })
    if err != nil {
      log.Fatal(err)
    }
    if res != nil {
      log.Printf("Link token: %s", res.LinkToken)
    }
  }
  ```

  ```java Java theme={"system"}
  import com.gr4vy.sdk.Gr4vy;
  import com.gr4vy.sdk.BearerSecuritySource;
  import com.gr4vy.sdk.Gr4vy.AvailableServers;
  import com.gr4vy.sdk.models.components.SessionRequest;
  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("access_token", "{{PLAID_ACCESS_TOKEN}}");

  var res = sdk.paymentServiceDefinitions().session()
      .action("create-link-token")
      .paymentServiceDefinitionId("plaid-bank")
      .xGr4vyPaymentMethod("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
      .requestBody(payload)
      .call();

  if (res.linkToken().isPresent()) {
      System.out.println("Link token: " + res.linkToken().get());
  }
  ```

  ```js TypeScript theme={"system"}
  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: {
        access_token: "{{PLAID_ACCESS_TOKEN}}",
      },
    },
    "plaid-bank",
    {
      headers: {
        "x-gr4vy-payment-method": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      },
    }
  );

  console.log(session.link_token);
  ```

  ```php PHP theme={"system"}
  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(
      action: 'create-link-token',
      paymentServiceDefinitionId: 'plaid-bank',
      xGr4vyPaymentMethod: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
      requestBody: [
          'access_token' => '{{PLAID_ACCESS_TOKEN}}'
      ]
  );

  echo "Link token: " . $response->linkToken;
  ```

  ```python Python theme={"system"}
  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(
          action="create-link-token",
          payment_service_definition_id="plaid-bank",
          x_gr4vy_payment_method="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
          request_body={
              "access_token": "{{PLAID_ACCESS_TOKEN}}"
          }
      )

      print(f"Link token: {res.link_token}")
  ```
</CodeGroup>

Present the returned Link token to the buyer for re-authentication. Once complete, retry the original transaction with the same stored payment method.

<Note>
  If the reauthorization happens outside of Plaid's Update Mode, the system sends a `payment-method.authorization-updated` webhook. When using Update Mode, only your frontend receives confirmation; no webhook is emitted. See [Plaid's Update Mode documentation](https://plaid.com/docs/api/items/#login_repaired) for details.
</Note>
