> ## 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.

# Adyen - Bank account (ACH, SEPA, BACS)

> Process ACH, SEPA, and BACS payments from bank accounts stored in Gr4vy through Adyen.

export const connector = {
  id: "adyen-bank",
  displayName: "Bank account (ACH, SEPA, BACS)",
  provider: "Adyen",
  method: "bank",
  mode: "bank",
  category: "bank",
  features: "bank_aggregators bank_details create_transaction deep_linking direct_capture partial_refunds payment_method_tokenization refunds requires_webhook_setup settlement_reporting settlement_reporting_from_webhook verify_credentials",
  supportedCountries: "AT BE BG CY CZ DE DK EE ES FI FR GB GR HR HU IE IT LT LU LV MT NL PL PT RO SE SI SK US",
  supportedCurrencies: "EUR GBP USD"
};

export const ConnectorRegions = ({data, kind, name: nameOverride}) => {
  const [query, setQuery] = useState("");
  const [open, setOpen] = useState(false);
  const isCountries = kind === "countries";
  const raw = data && (isCountries ? data.supportedCountries : data.supportedCurrencies);
  const codes = typeof raw === "string" ? raw.split(/\s+/).filter(Boolean) : Array.isArray(raw) ? raw : [];
  const DISPLAY_NAME_OVERRIDES = {
    authorizenet: "Authorize.net",
    cardpointe: "CardPointe",
    cybersource: "Cybersource",
    dlocal: "dLocal",
    shift4i4go: "Shift4 i4go",
    tokenex: "TokenEx"
  };
  const rawName = data && data.displayName || "";
  const name = nameOverride || DISPLAY_NAME_OVERRIDES[rawName.toLowerCase()] || rawName || "This connector";
  const verb = isCountries ? "supports transactions from buyers in" : "supports processing payments in";
  const noun = isCountries ? "countries" : "currencies";
  if (codes.length === 0) return null;
  const unrestricted = codes.length >= (isCountries ? 200 : 150);
  const disclaimer = <p style={{
    opacity: 0.7,
    fontSize: "0.8125rem",
    marginTop: "0.75rem"
  }}>
      This list is for guidance only. It doesn't guarantee availability or
      create any rights, and the provider must also enable each of these {noun}{" "}
      for your account.
    </p>;
  let displayNames = null;
  try {
    displayNames = new Intl.DisplayNames(["en"], {
      type: isCountries ? "region" : "currency"
    });
  } catch (e) {
    displayNames = null;
  }
  const resolve = code => {
    if (!displayNames) return null;
    try {
      const resolved = displayNames.of(code);
      return resolved && resolved !== code ? resolved : null;
    } catch (e) {
      return null;
    }
  };
  const MAJOR_CURRENCIES = ["USD", "EUR", "GBP", "CAD", "AUD", "JPY", "CHF", "CNY", "SGD", "HKD", "NZD", "SEK", "NOK", "DKK", "MXN", "BRL", "INR"];
  const items = codes.map(code => ({
    code,
    label: resolve(code)
  }));
  if (isCountries) {
    items.sort((a, b) => (a.label || a.code).localeCompare(b.label || b.code));
  } else {
    const rank = code => {
      const i = MAJOR_CURRENCIES.indexOf(code);
      return i === -1 ? MAJOR_CURRENCIES.length : i;
    };
    items.sort((a, b) => rank(a.code) - rank(b.code) || a.code.localeCompare(b.code));
  }
  if (codes.length <= 3) {
    const parts = items.map(it => isCountries || !it.label ? it.label || it.code : `${it.label} (${it.code})`);
    const joined = parts.length === 1 ? parts[0] : parts.length === 2 ? `${parts[0]} and ${parts[1]}` : `${parts.slice(0, -1).join(", ")}, and ${parts[parts.length - 1]}`;
    return <div>
        <p>
          {name} {verb} {joined}.
        </p>
        {disclaimer}
      </div>;
  }
  const chipStyle = {
    display: "inline-flex",
    alignItems: "baseline",
    gap: "0.4rem",
    padding: "0.15rem 0.55rem",
    borderRadius: "0.375rem",
    border: "1px solid rgba(128, 128, 128, 0.25)",
    fontSize: "0.875rem",
    lineHeight: 1.5
  };
  const codeStyle = {
    fontFamily: "var(--font-mono, ui-monospace, monospace)",
    fontWeight: 600,
    fontSize: "0.8125rem"
  };
  const controlStyle = {
    color: "inherit",
    background: "transparent",
    border: "1px solid rgba(128, 128, 128, 0.3)",
    borderRadius: "0.5rem",
    fontSize: "0.875rem"
  };
  const renderChip = it => <span key={it.code} style={chipStyle} title={isCountries ? it.code : it.label || it.code}>
      {isCountries ? it.label || it.code : <span style={codeStyle}>{it.code}</span>}
      {!isCountries && it.label ? <span style={{
    opacity: 0.7
  }}>{it.label}</span> : null}
    </span>;
  const PREVIEW = 5;
  const collapsible = items.length > PREVIEW;
  const q = query.trim().toLowerCase();
  const filtered = q ? items.filter(it => it.code.toLowerCase().includes(q) || it.label && it.label.toLowerCase().includes(q)) : items;
  const expanded = open || q !== "";
  const visible = !collapsible ? items : expanded ? filtered : items.slice(0, PREVIEW);
  const toggle = () => {
    const next = !open;
    setOpen(next);
    if (!next) setQuery("");
  };
  return <div>
      {unrestricted ? <p>
          Gr4vy doesn't limit {name} to specific {noun}. Check with the provider
          which {noun} it supports for your account.
        </p> : <p>
          {name} {verb} the following {codes.length} {noun}:
        </p>}

      {collapsible ? <input type="text" value={query} onChange={e => setQuery(e.target.value)} placeholder={`Filter ${noun}…`} aria-label={`Filter ${noun}`} style={{
    ...controlStyle,
    display: "block",
    width: "100%",
    maxWidth: "22rem",
    padding: "0.4rem 0.7rem",
    margin: "0 0 0.75rem"
  }} /> : null}

      <div style={{
    display: "flex",
    flexWrap: "wrap",
    gap: "0.4rem"
  }}>
        {visible.map(renderChip)}
      </div>

      {q && filtered.length === 0 ? <p style={{
    opacity: 0.7,
    marginTop: "0.6rem"
  }}>
          No {noun} match “{query.trim()}”.
        </p> : null}
      {q && filtered.length > 0 ? <p style={{
    opacity: 0.6,
    fontSize: "0.8125rem",
    marginTop: "0.6rem"
  }}>
          Showing {filtered.length} of {items.length}.
        </p> : null}

      {collapsible && !q ? <button type="button" aria-expanded={open} onClick={toggle} style={{
    ...controlStyle,
    display: "inline-flex",
    alignItems: "center",
    gap: "0.4rem",
    padding: "0.35rem 0.75rem",
    marginTop: "0.75rem",
    cursor: "pointer"
  }}>
          <span aria-hidden="true" style={{
    display: "inline-block",
    transform: open ? "rotate(90deg)" : "none",
    transition: "transform 0.15s ease"
  }}>
            ›
          </span>
          {open ? "Show fewer" : `and ${items.length - PREVIEW} more`}
        </button> : null}

      {disclaimer}
    </div>;
};

export const ConnectorCapabilities = ({data}) => {
  const CAPABILITIES = [{
    keys: ["three_d_secure_pass_through"],
    label: "3-D Secure",
    description: "Gr4vy runs the 3DS authentication and sends the results to the provider on the authorization.",
    cardOnly: true
  }, {
    keys: ["three_d_secure_hosted"],
    label: "3-D Secure (provider-hosted)",
    description: "The provider runs the 3DS authentication itself, redirecting the buyer to its own page.",
    cardOnly: true
  }, {
    keys: ["partial_authorization"],
    label: "Partial authorization",
    description: "Support partial approval responses."
  }, {
    keys: ["zero_auth"],
    label: "Zero auth",
    description: "Verify a payment method without charging it."
  }, {
    keys: ["void"],
    label: "Void",
    description: "Cancel an authorized transaction before capture."
  }, {
    keys: ["incremental_authorization"],
    label: "Incremental authorization",
    description: "Increase the amount of an existing authorization before capture."
  }, {
    keys: ["direct_capture"],
    label: "Direct capture",
    description: "Capture a payment immediately at authorization.",
    hideWhenUnsupported: true
  }, {
    keys: ["delayed_capture"],
    label: "Delayed capture",
    description: "Authorize a payment and capture it at a later time."
  }, {
    keys: ["partial_capture"],
    label: "Partial capture",
    description: "Capture a portion of the authorized amount."
  }, {
    keys: ["over_capture"],
    label: "Over capture",
    description: "Capture more than the originally authorized amount."
  }, {
    keys: ["multi_capture"],
    label: "Multiple captures",
    description: "Capture one authorization in several partial captures."
  }, {
    keys: ["tracking_details"],
    label: "Shipment tracking",
    description: "Forward shipment tracking details to the provider on capture."
  }, {
    keys: ["refunds"],
    label: "Refunds",
    description: "Refund a captured payment."
  }, {
    keys: ["partial_refunds"],
    label: "Partial refunds",
    description: "Refund a portion of the captured amount."
  }, {
    keys: ["settlement_reporting"],
    label: "Settlement reporting",
    description: "Automatic settlement and reconciliation reporting."
  }, {
    keys: ["create_session"],
    label: "Create session",
    description: "Create a connector session for client-side flows."
  }, {
    keys: ["network_tokens_default", "network_tokens_toggle"],
    label: "Network tokens",
    description: "Network-level tokenization for improved approval rates.",
    cardOnly: true
  }, {
    keys: ["open_loop", "open_loop_toggle"],
    label: "Open loop",
    description: "Charge stored cards with card data from the Gr4vy vault, so merchant-initiated payments aren't tied to the connection that stored the card.",
    optionalKey: "open_loop_toggle",
    optionalNote: "Off by default. The provider must allow it before you turn it on for the connection.",
    cardOnly: true
  }, {
    keys: ["issuer_based_installments"],
    label: "Issuer installments",
    description: "Ask the issuer to split a single payment into installments with installment_count.",
    cardOnly: true,
    hideWhenUnsupported: true
  }, {
    keys: ["digital_wallets"],
    label: "Digital wallets",
    description: "Apple Pay, Google Pay, and other wallet integrations."
  }, {
    keys: ["payment_method_tokenization", "payment_method_tokenization_toggle"],
    label: "Payment method tokenization",
    description: "Store payment methods outside of transactions."
  }, {
    keys: ["transaction_sync"],
    label: "Transaction sync",
    description: "Synchronize transaction state from the connector."
  }, {
    keys: ["create_token"],
    label: "Tokenization",
    description: "Create a token from card details collected via Secure Fields.",
    hideWhenUnsupported: true
  }, {
    keys: ["delete_token"],
    label: "Delete token",
    description: "Delete a stored token.",
    hideWhenUnsupported: true
  }, {
    keys: ["verify_credentials"],
    label: "Verify credentials",
    description: "Validate the configured credentials against the connector.",
    hideWhenUnsupported: true
  }];
  const raw = data && data.features;
  const enabled = typeof raw === "string" ? new Set(raw.split(/\s+/).filter(Boolean)) : Array.isArray(raw) ? new Set(raw) : new Set(Object.keys(raw || ({})).filter(key => raw[key]));
  const isOn = entry => entry.keys.some(key => enabled.has(key));
  const isNonCard = data && data.method && data.method !== "card";
  const renderGroup = (title, entries, supported) => {
    if (entries.length === 0) return null;
    const mark = supported ? "✓" : "✕";
    const markColor = supported ? "#16a34a" : "#9ca3af";
    return <div style={{
      marginTop: "1rem"
    }}>
        <div style={{
      fontSize: "0.75rem",
      fontWeight: 600,
      letterSpacing: "0.05em",
      textTransform: "uppercase",
      opacity: 0.6,
      marginBottom: "0.25rem"
    }}>
          {title}
        </div>
        {}
        <div role="list">
          {entries.map(entry => <div role="listitem" key={entry.label} style={{
      display: "flex",
      gap: "0.5rem",
      alignItems: "baseline",
      padding: "0.3rem 0",
      opacity: supported ? 1 : 0.7
    }}>
              <span aria-hidden="true" style={{
      color: markColor,
      fontWeight: 700,
      flexShrink: 0
    }}>
                {mark}
              </span>
              <span>
                <strong>{entry.label}</strong>
                {entry.description ? <span style={{
      opacity: 0.85
    }}> — {entry.description}</span> : null}
                {supported && entry.optionalKey && entry.keys.every(key => key === entry.optionalKey || !enabled.has(key)) ? <span style={{
      opacity: 0.85
    }}> {entry.optionalNote}</span> : null}
              </span>
            </div>)}
        </div>
      </div>;
  };
  const visible = isNonCard ? CAPABILITIES.filter(entry => !entry.cardOnly) : CAPABILITIES;
  const supported = visible.filter(isOn);
  const unsupported = visible.filter(entry => !isOn(entry) && !entry.hideWhenUnsupported);
  return <div>
      {renderGroup("Supported", supported, true)}
      {renderGroup("Not supported", unsupported, false)}
      <p style={{
    opacity: 0.7,
    fontSize: "0.8125rem"
  }}>
        This list is for guidance only. It doesn't guarantee availability or
        create any rights, and the provider may need to enable some features for
        your account.
      </p>
    </div>;
};

export const ConnectorHeader = ({data, name: nameProp, method: methodName, logo, category: categoryProp, catalog, providers}) => {
  const [logoStage, setLogoStage] = useState(0);
  const [chipLogoFailed, setChipLogoFailed] = useState(false);
  const [anchors, setAnchors] = useState({});
  useEffect(() => {
    const found = {};
    for (const id of ["capabilities", "supported-countries", "supported-currencies", "supported-payment-methods"]) {
      found[id] = Boolean(document.getElementById(id));
    }
    setAnchors(found);
  }, []);
  const hasData = Boolean(data && data.id);
  if (!hasData && !(nameProp && logo)) return null;
  const CATEGORY_LABELS = {
    card: "Cards",
    bank: "Banking",
    bnpl: "BNPL",
    cash: "Cash vouchers",
    crypto: "Crypto",
    wallet: "Wallets",
    tokenization: "Tokenization"
  };
  const DISPLAY_NAME_OVERRIDES = {
    authorizenet: "Authorize.net",
    cardpointe: "CardPointe",
    cybersource: "Cybersource",
    dlocal: "dLocal",
    shift4i4go: "Shift4 i4go",
    tokenex: "TokenEx",
    "worldine travelhub": "Worldline TravelHub"
  };
  const HIGHLIGHTS = [{
    keys: ["three_d_secure_pass_through", "three_d_secure_hosted"],
    label: "3-D Secure",
    cardOnly: true
  }, {
    keys: ["network_tokens_default", "network_tokens_toggle"],
    label: "Network tokens",
    cardOnly: true
  }, {
    keys: ["digital_wallets"],
    label: "Digital wallets"
  }, {
    keys: ["delayed_capture"],
    label: "Delayed capture"
  }, {
    keys: ["partial_capture"],
    label: "Partial capture"
  }, {
    keys: ["refunds"],
    label: "Refunds"
  }, {
    keys: ["partial_refunds"],
    label: "Partial refunds"
  }, {
    keys: ["zero_auth"],
    label: "Zero auth"
  }, {
    keys: ["settlement_reporting"],
    label: "Settlement reporting"
  }];
  const d = hasData ? data : {};
  const method = d.method || "";
  const provider = !hasData ? "" : method && d.id.endsWith(`-${method}`) ? d.id.slice(0, -(method.length + 1)) : d.id.split("-")[0];
  const fix = value => DISPLAY_NAME_OVERRIDES[(value || "").toLowerCase()] || value || "";
  const name = fix(d.provider) || nameProp || fix(d.displayName) || provider;
  const displayName = fix(d.displayName);
  const productName = !hasData ? "" : method === "card" ? d.provider ? displayName : "" : methodName || (d.provider && displayName !== name ? displayName : "");
  const methodLed = Boolean(productName);
  const cardProduct = methodLed && method === "card";
  const category = categoryProp || (method === "card" && !cardProduct ? "" : CATEGORY_LABELS[d.category] || "");
  const flowLabel = d.category !== "bank" ? "" : d.mode === "bank" ? "Stored in Gr4vy" : d.mode === "redirect" ? "Redirect" : "";
  const providerLogo = hasData ? `/assets/connectors/providers/${provider}.svg` : logo;
  const methodLogo = `/assets/connectors/methods/${method}.svg`;
  const title = methodLed ? productName : name;
  const chipLabel = !hasData ? "" : methodLed ? `via ${name}` : methodName;
  const chipLogo = cardProduct ? "" : methodLed ? providerLogo : methodLogo;
  const logos = methodLed && !cardProduct ? [methodLogo, providerLogo] : [providerLogo];
  const primaryLogo = logos[logoStage] || null;
  const enabled = new Set((d.features || "").split(/\s+/).filter(Boolean));
  const highlights = HIGHLIGHTS.filter(h => !(h.cardOnly && method !== "card") && h.keys.some(k => enabled.has(k)));
  const codes = value => (value || "").split(/\s+/).filter(Boolean);
  const providerSet = new Set(codes(providers));
  const members = !hasData && catalog && providerSet.size > 0 ? catalog.filter(e => providerSet.has(e.provider)) : [];
  const union = key => new Set(members.flatMap(e => codes(e[key]))).size;
  const methodsCount = members.length;
  const countries = hasData ? codes(d.supportedCountries).length : union("countries");
  const currencies = hasData ? codes(d.supportedCurrencies).length : union("currencies");
  const border = "1px solid rgba(128, 128, 128, 0.2)";
  const labelStyle = {
    fontSize: "0.6875rem",
    fontWeight: 600,
    letterSpacing: "0.05em",
    textTransform: "uppercase",
    opacity: 0.6
  };
  const chipStyle = {
    display: "inline-flex",
    alignItems: "center",
    gap: "0.3rem",
    padding: "0.1rem 0.5rem",
    borderRadius: "999px",
    border,
    fontSize: "0.8125rem",
    whiteSpace: "nowrap"
  };
  const renderStat = (label, value, anchor) => <a href={anchors[anchor] ? `#${anchor}` : undefined} style={{
    display: "flex",
    flexDirection: "column",
    gap: "0.1rem",
    textDecoration: "none",
    borderBottom: "none",
    color: "inherit",
    minWidth: "6rem"
  }}>
      <span style={labelStyle}>{label}</span>
      <span style={{
    fontSize: "1.25rem",
    fontWeight: 600
  }}>{value}</span>
    </a>;
  return <div className="not-prose" style={{
    border,
    borderRadius: "0.75rem",
    padding: "1.25rem",
    margin: "1rem 0 1.5rem",
    display: "flex",
    flexDirection: "column",
    gap: "1rem"
  }}>
      <div style={{
    display: "flex",
    alignItems: "center",
    gap: "1rem"
  }}>
        {primaryLogo ? <img src={primaryLogo} alt={`${title} logo`} width="56" height="56" onError={() => setLogoStage(logoStage + 1)} style={{
    width: "3.5rem",
    height: "3.5rem",
    borderRadius: "0.5rem",
    margin: 0,
    flexShrink: 0
  }} /> : <span aria-hidden="true" style={{
    width: "3.5rem",
    height: "3.5rem",
    borderRadius: "0.5rem",
    border,
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    fontSize: "1.5rem",
    fontWeight: 600,
    flexShrink: 0
  }}>
            {title.charAt(0).toUpperCase()}
          </span>}
        <div style={{
    display: "flex",
    flexDirection: "column",
    gap: "0.35rem",
    minWidth: 0
  }}>
          <span style={{
    fontSize: "1.25rem",
    fontWeight: 600,
    lineHeight: 1.2
  }}>{title}</span>
          <span style={{
    display: "flex",
    flexWrap: "wrap",
    gap: "0.4rem",
    alignItems: "center"
  }}>
            {chipLabel ? <span style={chipStyle}>
                {chipLogoFailed || !chipLogo ? null : <img src={chipLogo} alt="" width="16" height="16" onError={() => setChipLogoFailed(true)} style={{
    width: "1rem",
    height: "1rem",
    borderRadius: "0.2rem",
    margin: 0
  }} />}
                {chipLabel}
              </span> : null}
            {category ? <span style={{
    ...chipStyle,
    opacity: 0.75
  }}>{category}</span> : null}
            {flowLabel ? <span style={{
    ...chipStyle,
    opacity: 0.75
  }}>{flowLabel}</span> : null}
          </span>
        </div>
      </div>

      {methodsCount > 0 || countries > 0 || currencies > 0 || highlights.length > 0 ? <div style={{
    display: "flex",
    flexWrap: "wrap",
    gap: "1rem 2rem",
    paddingTop: "1rem",
    borderTop: border
  }}>
        {methodsCount > 0 ? renderStat("Payment methods", methodsCount, "supported-payment-methods") : null}
        {}
        {countries > 0 ? renderStat("Countries", countries >= 200 ? "No limit" : countries, "supported-countries") : null}
        {currencies > 0 ? renderStat("Currencies", currencies >= 150 ? "No limit" : currencies, "supported-currencies") : null}
        {highlights.length > 0 ? <div style={{
    display: "flex",
    flexDirection: "column",
    gap: "0.35rem",
    flex: "1 1 16rem"
  }}>
            <span style={labelStyle}>Highlights</span>
            <span style={{
    display: "flex",
    flexWrap: "wrap",
    gap: "0.35rem"
  }}>
              {highlights.map(h => <span key={h.label} style={chipStyle}>
                  <span aria-hidden="true" style={{
    color: "#16a34a",
    fontWeight: 700
  }}>✓</span>
                  {h.label}
                </span>)}
              {anchors.capabilities ? <a href="#capabilities" style={{
    ...chipStyle,
    borderStyle: "dashed",
    textDecoration: "none",
    color: "inherit"
  }}>
                  All capabilities →
                </a> : null}
            </span>
          </div> : null}
      </div> : null}
    </div>;
};

<ConnectorHeader data={connector} name="Adyen" method="Bank account (ACH, SEPA, BACS)" />

Adyen is a global payment technology company founded in 2006 in Amsterdam, Netherlands. The company provides a single platform for accepting payments across online, mobile, and in-store channels for major enterprises worldwide including Uber, Spotify, and Microsoft.

Adyen's bank account connector processes ACH, SEPA, and BACS payments from bank accounts that Gr4vy stores. It supports two ways to get the account:

* **Plaid Link**—Customers link their bank account through Plaid, and the resulting token is used to process the payment. Adyen only processes Plaid-linked payments for ACH Direct Debit accounts.
* **Bank account details**—You provide the bank account details via the API. This flow supports ACH, SEPA, and BACS schemes. See [Bank accounts](/connections/payments/bank) for details.

<Note>
  **Four Adyen connectors debit a buyer's bank account.** Pick the one that matches who collects the account:

  * [ACH Direct Debit](/connections/payments/adyen-ach) (redirect): the buyer enters their account and routing numbers on Adyen's page, and Adyen keeps them.
  * [SEPA](/connections/payments/adyen-sepa) (redirect): the buyer enters their IBAN on Adyen's page, and Adyen keeps it.
  * [Pay by Bank (redirect)](/connections/payments/adyen-paybybank): Adyen sends the buyer to Plaid to choose their bank. US only.
  * [Bank account (ACH, SEPA, BACS)](/connections/payments/adyen-bank) (stored in Gr4vy): you send the bank account details or a Plaid-linked account, and Gr4vy stores the account. Use this connector to charge bank accounts that you [import](/guides/features/token-import/overview) into Gr4vy.
</Note>

## Setup

Please follow the [common Adyen instructions](./adyen) to get set up with Adyen. Next, make sure to enable the relevant bank payment methods (ACH, SEPA, or BACS) on your configured Adyen account.

The following additional settings are available.

* **Force direct bank details**—When enabled, the system skips attempting to exchange a Plaid processor token with Adyen and instead fetches raw bank details (account number, routing number, IBAN, or sort code) from Plaid directly. This saves an API call per payment and is useful when you prefer to always process with raw bank details. This setting has no effect on transactions that already use the direct `bank` payment method.

## Prerequisites

To use bank payments via Adyen, you must have:

1. **Adyen bank payments enabled:** The relevant bank payment method (ACH, SEPA, or BACS) must be enabled in your Adyen account
2. **Plaid configured:** For Plaid-linked payments, set up [Plaid](./plaid-bank) in your Gr4vy dashboard to handle bank account linking

For Plaid-linked payments, the integration flow requires customers to first link their bank account through Plaid Link, then use the resulting public token to create a transaction that is processed through Adyen.

For bank account details, you submit them via the API without requiring Plaid.

## Capabilities

<ConnectorCapabilities data={connector} />

## Supported countries

<ConnectorRegions data={connector} kind="countries" name="Bank account (ACH, SEPA, BACS)" />

## Supported currencies

<ConnectorRegions data={connector} kind="currencies" name="Bank account (ACH, SEPA, BACS)" />

## Billing details

Some schemes need billing details on the buyer or transaction. When a required field is missing, the transaction fails with the `missing_billing_address` error code.

* **ACH**: Include the billing address `state_code` in ISO 3166-2 format, for example `US-UT`. Gr4vy sends Adyen's state or province from `state_code` only, not from the free-text `state`. See [Billing address](/connections/payments/adyen#billing-address).
* **SEPA**: For an IBAN from a SEPA country outside the European Economic Area, such as `CH` or `GB`, include a billing address with `line1`, `house_number_or_name`, `postal_code`, `city`, and `country`.
* **BACS**: Include the buyer's billing email address.

## Limitations

### Refunds

Partial refunds are not supported for unsettled bank transactions. If you need to refund a bank transaction before it has settled, you must refund the full transaction amount. Once a transaction has settled, partial refunds become available.

### Payment methods

This connector supports bank accounts linked through Plaid (ACH, SEPA, and BACS) and bank account details submitted via the API. See [Bank accounts](/connections/payments/bank) for that flow.

## Usage

You can create bank payments via Adyen using either Plaid Link or bank account details.

### Option 1: Plaid Link

First, use Plaid Link to securely connect the customer's bank account. See the [Plaid integration guide](./plaid-bank-integration) for detailed instructions on implementing Plaid Link.

When the customer successfully links their account, you'll receive a `publicToken` and account metadata from Plaid.

Use the `publicToken` from Plaid to create a transaction that is processed through the Adyen bank connector.

<CodeGroup>
  ```csharp C# theme={"system"}
  var transaction = await client.Transactions.CreateAsync(
      transactionCreate: new TransactionCreate()
      {
          Amount = 1299,
          Currency = "USD",
          Country = "US",
          Intent = "capture",
          // The method discriminator defaults to "plaid" and isn't set explicitly
          PaymentMethod = TransactionCreatePaymentMethod.CreatePlaidPaymentMethodCreate(
              new PlaidPaymentMethodCreate()
              {
                  Token = "public-sandbox-xxxxx",
                  AccountId = "account-id-from-metadata"
              }
          ),
          Buyer = new GuestBuyer()
          {
              BillingDetails = new BillingDetails()
              {
                  FirstName = "John",
                  LastName = "Doe"
              }
          },
          PaymentServiceId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
      }
  );
  ```

  ```go Go theme={"system"}
  amount := int64(1299)
  currency := "USD"
  country := "US"
  paymentServiceId := "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

  // The method discriminator defaults to "plaid" and isn't set explicitly
  plaidPaymentMethodCreate := components.PlaidPaymentMethodCreate{
      Token:     "public-sandbox-xxxxx",
      AccountID: gr4vy.String("account-id-from-metadata"),
  }
  paymentMethod := components.CreateTransactionCreatePaymentMethodPlaidPaymentMethodCreate(plaidPaymentMethodCreate)

  transactionCreate := components.TransactionCreate{
      Amount:        amount,
      Currency:      currency,
      Country:       &country,
      Intent:        gr4vy.Pointer(components.TransactionIntentCapture),
      PaymentMethod: &paymentMethod,
      Buyer: &components.GuestBuyer{
          BillingDetails: &components.BillingDetails{
              FirstName: gr4vy.String("John"),
              LastName:  gr4vy.String("Doe"),
          },
      },
      PaymentServiceID: &paymentServiceId,
  }

  transaction, err := client.Transactions.Create(ctx, transactionCreate, nil, nil, nil)
  ```

  ```java Java theme={"system"}
  CreateTransactionResponse transactionResponse = gr4vyClient.transactions().create()
      .transactionCreate(TransactionCreate.builder()
          .amount(1299L)
          .currency("USD")
          .country("US")
          .intent(TransactionIntent.CAPTURE)
          // The method discriminator defaults to "plaid" and isn't set explicitly
          .paymentMethod(TransactionCreatePaymentMethod.of(PlaidPaymentMethodCreate.builder()
              .token("public-sandbox-xxxxx")
              .accountId("account-id-from-metadata")
              .build()))
          .buyer(GuestBuyer.builder()
              .billingDetails(BillingDetails.builder()
                  .firstName("John")
                  .lastName("Doe")
                  .build())
              .build())
          .paymentServiceId("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
          .build())
      .call();

  Transaction transaction = transactionResponse.transaction().orElse(null);
  ```

  ```php PHP theme={"system"}
  $transactionCreate = new TransactionCreate(
      amount: 1299,
      currency: 'USD',
      country: 'US',
      intent: 'capture',
      // The method discriminator defaults to "plaid" and isn't set explicitly
      paymentMethod: new PlaidPaymentMethodCreate(
          token: 'public-sandbox-xxxxx',
          accountId: 'account-id-from-metadata'
      ),
      buyer: new GuestBuyer(
          billingDetails: new BillingDetails(
              firstName: 'John',
              lastName: 'Doe'
          )
      ),
      paymentServiceId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
  );
  $response = self::$sdk->transactions->create($transactionCreate);
  $transaction = $response->transaction;
  ```

  ```python Python theme={"system"}
  transaction: models.Transaction = client.transactions.create(
      amount=1299,
      currency="USD",
      country="US",
      intent="capture",
      # The method discriminator defaults to "plaid" and isn't set explicitly
      payment_method=models.PlaidPaymentMethodCreate(
          token="public-sandbox-xxxxx",
          account_id="account-id-from-metadata"
      ),
      buyer=models.GuestBuyer(
          billing_details=models.BillingDetails(
              first_name="John",
              last_name="Doe"
          )
      ),
      payment_service_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  )
  ```

  ```ts TypeScript theme={"system"}
  const transaction = await gr4vy.transactions.create({
      amount: 1299,
      currency: "USD",
      country: "US",
      intent: "capture",
      paymentMethod: {
          method: "plaid",
          token: "public-sandbox-xxxxx",
          accountId: "account-id-from-metadata"
      },
      buyer: {
          billingDetails: {
              firstName: "John",
              lastName: "Doe"
          }
      },
      paymentServiceId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  })
  ```
</CodeGroup>

**Key parameters:**

* `method` - Set to `plaid` to indicate a Plaid-linked bank account
* `token` - The public token received from Plaid Link
* `account_id` - Optional. The specific account ID from Plaid metadata if the customer linked multiple accounts
* `payment_service_id` - The UUID of your Adyen bank connection (found in Gr4vy dashboard)

The transaction is processed through Adyen as an ACH Direct Debit payment using the bank account details securely retrieved from Plaid.

### Option 2: Bank account details

Submit raw bank account details directly via the API. Set `method` to `bank` and `scheme` to the appropriate value (`ach`, `sepa`, or `bacs`).

The following example uses the `ach` scheme. For SEPA, replace the scheme with `sepa` and provide the IBAN as the `account_number`. For BACS, replace the scheme with `bacs` and provide the sort code as the `routing_number`.

<CodeGroup>
  ```csharp C# theme={"system"}
  var transaction = await client.Transactions.CreateAsync(
      transactionCreate: new TransactionCreate()
      {
          Amount = 1299,
          Currency = "USD",
          Country = "US",
          Intent = "capture",
          PaymentMethod = TransactionCreatePaymentMethod.CreateACHBankPaymentMethodCreate(
              new ACHBankPaymentMethodCreate()
              {
                  AccountNumber = "1234567890",
                  RoutingNumber = "011000138",
                  AccountType = "checking",
                  AccountHolder = new BankAccountHolder()
                  {
                      FirstName = "John",
                      LastName = "Doe"
                  }
              }
          ),
          PaymentServiceId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
      }
  );
  ```

  ```go Go theme={"system"}
  amount := int64(1299)
  currency := "USD"
  country := "US"
  paymentServiceId := "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

  achPaymentMethod := components.ACHBankPaymentMethodCreate{
      AccountNumber: "1234567890",
      RoutingNumber: "011000138",
      AccountType:   components.AccountTypeChecking,
      AccountHolder: components.BankAccountHolder{
          FirstName: gr4vy.String("John"),
          LastName:  gr4vy.String("Doe"),
      },
  }
  paymentMethod := components.CreateTransactionCreatePaymentMethodACHBankPaymentMethodCreate(achPaymentMethod)

  transactionCreate := components.TransactionCreate{
      Amount:           amount,
      Currency:         currency,
      Country:          &country,
      Intent:           gr4vy.Pointer(components.TransactionIntentCapture),
      PaymentMethod:    &paymentMethod,
      PaymentServiceID: &paymentServiceId,
  }

  transaction, err := client.Transactions.Create(ctx, transactionCreate, nil, nil, nil)
  ```

  ```java Java theme={"system"}
  CreateTransactionResponse transactionResponse = gr4vyClient.transactions().create()
      .transactionCreate(TransactionCreate.builder()
          .amount(1299L)
          .currency("USD")
          .country("US")
          .intent(TransactionIntent.CAPTURE)
          .paymentMethod(TransactionCreatePaymentMethod.of(ACHBankPaymentMethodCreate.builder()
              .accountNumber("1234567890")
              .routingNumber("011000138")
              .accountType(AccountType.CHECKING)
              .accountHolder(BankAccountHolder.builder()
                  .firstName("John")
                  .lastName("Doe")
                  .build())
              .build()))
          .paymentServiceId("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
          .build())
      .call();

  Transaction transaction = transactionResponse.transaction().orElse(null);
  ```

  ```php PHP theme={"system"}
  $transactionCreate = new TransactionCreate(
      amount: 1299,
      currency: 'USD',
      country: 'US',
      intent: 'capture',
      paymentMethod: new ACHBankPaymentMethodCreate(
          accountNumber: '1234567890',
          routingNumber: '011000138',
          accountType: 'checking',
          accountHolder: new BankAccountHolder(
              firstName: 'John',
              lastName: 'Doe'
          )
      ),
      paymentServiceId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
  );
  $response = $sdk->transactions->create($transactionCreate);
  $transaction = $response->transaction;
  ```

  ```python Python theme={"system"}
  transaction = client.transactions.create(
      amount=1299,
      currency="USD",
      country="US",
      intent="capture",
      payment_method={
          "method": "bank",
          "scheme": "ach",
          "account_number": "1234567890",
          "routing_number": "011000138",
          "account_type": "checking",
          "account_holder": {
              "first_name": "John",
              "last_name": "Doe"
          }
      },
      payment_service_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  )
  ```

  ```ts TypeScript theme={"system"}
  const transaction = await client.transactions.create({
      amount: 1299,
      currency: "USD",
      country: "US",
      intent: "capture",
      paymentMethod: {
          method: "bank",
          scheme: "ach",
          accountNumber: "1234567890",
          routingNumber: "011000138",
          accountType: "checking",
          accountHolder: {
              firstName: "John",
              lastName: "Doe"
          }
      },
      paymentServiceId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  });
  ```
</CodeGroup>

For vaulting, recurring payments, and additional details, see the [Bank accounts](/connections/payments/bank) guide.

## Testing

In a test environment, you can use Plaid's sandbox mode along with Adyen's test environment:

1. Configure Plaid with sandbox credentials
2. Use Plaid's test credentials to link a test bank account
3. Process test transactions through Adyen's sandbox

For bank account details, use Adyen's test account numbers for ACH, SEPA, or BACS as documented in Adyen's testing guides.

Refer to [Plaid's testing documentation](https://plaid.com/docs/sandbox/) for available test credentials and scenarios.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.