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

# BlueSnap - ACH Direct Debit

> Connect to BlueSnap to accept ACH Direct Debit bank payments in the US.

export const connector = {
  displayName: "BlueSnap ACH",
  method: "ach",
  features: "create_transaction direct_capture direct_integration_create partial_refunds payment_method_tokenization redirect_requires_popup refunds verify_credentials",
  supportedCountries: "US",
  supportedCurrencies: "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: "Fiserv CardPointe",
    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;
  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 <p>
        {name} {verb} {joined}.
      </p>;
  }
  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>
      <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}
    </div>;
};

export const ConnectorCapabilities = ({data}) => {
  const CAPABILITIES = [{
    keys: ["three_d_secure_hosted"],
    label: "3-D Secure (hosted)",
    description: "Gr4vy-hosted 3DS authentication flow.",
    cardOnly: true
  }, {
    keys: ["three_d_secure_pass_through"],
    label: "3-D Secure (pass-through)",
    description: "Pass through 3DS data authenticated by a third party.",
    cardOnly: true
  }, {
    keys: ["partial_authorization"],
    label: "Partial authorization",
    description: "Support partial approval responses."
  }, {
    keys: ["zero_auth"],
    label: "Zero auth",
    description: "Verify a card without charging it."
  }, {
    keys: ["void"],
    label: "Void",
    description: "Cancel an authorized transaction 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: ["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: ["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}
              </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)}
    </div>;
};

BlueSnap is a global payment platform that provides payment processing solutions for merchants worldwide. The ACH Direct Debit integration enables US merchants to accept bank account payments directly. Buyers enter their bank account and routing numbers in a BlueSnap-hosted form embedded as a popup, and funds are debited via the ACH network. ACH payments are asynchronous — the transaction enters a pending state immediately after the buyer submits their details, and settlement typically takes 1–3 business days.

## Setup

BlueSnap provides a self-service sign-up for a sandbox account. To sign up, visit the [BlueSnap sign-up page](https://sandbox.bluesnap.com/jsp/onboarding/) and fill in the required details.

### Credentials

When setting up BlueSnap ACH in the dashboard, configure the following credentials:

* **API User Key** - Your BlueSnap API User Key
* **API Password** - Your BlueSnap API Password
* **Webhook Security Header** - Optional security header for webhook signature validation

## Connector configuration

After setting up your BlueSnap ACH connector in the dashboard, configure how transactions are routed to it. Choose one of the following options:

* **Using Flow** - Configure BlueSnap ACH as the target connector in [Flow](/guides/dashboard/flow/overview) to automatically route ACH transactions to this connector
* **Using the API** - Explicitly set the `payment_service_id` parameter to the BlueSnap ACH connector ID when creating transactions. This overrides any Flow routing rules.

The connector ID can be found in the dashboard under **Connections** -> **Configured connections**.

## Capabilities

<ConnectorCapabilities data={connector} />

## Supported countries

<ConnectorRegions data={connector} kind="countries" />

## Supported currencies

<ConnectorRegions data={connector} kind="currencies" />

## Integration

BlueSnap ACH uses a redirect integration where BlueSnap's hosted bank account form is loaded in a popup on your page. The buyer enters their account and routing numbers in the popup, and Gr4vy handles communication with BlueSnap to complete the payment.

ACH settlement is asynchronous. After the buyer submits the form the transaction enters a `pending` state. BlueSnap sends webhooks as the payment progresses:

| Event            | Description                                                                   |
| ---------------- | ----------------------------------------------------------------------------- |
| `CHARGE_PENDING` | The bank debit has been submitted and is awaiting settlement.                 |
| `CHARGE`         | Settlement confirmed. Funds have been captured from the buyer's bank account. |
| `DECLINE`        | The debit was rejected by the buyer's bank.                                   |
| `FRAUD_DECLINE`  | The transaction was declined due to fraud screening.                          |
| `REFUND`         | A refund has been processed successfully.                                     |

Configure your BlueSnap account to send IPN notifications to your Gr4vy webhook endpoint. The webhook security header is optional but recommended to validate that notifications originate from BlueSnap.

### Redirect integration

Start by creating a new transaction with the following required fields.

<CodeGroup>
  ```csharp C# theme={"system"}
  var transaction = await client.Transactions.CreateAsync(
      transactionCreate: new TransactionCreate()
      {
          Amount = 1299,
          Currency = "USD",
          Country = "US",
          Intent = "capture",
          PaymentMethod =
              TransactionCreatePaymentMethod.CreateRedirectPaymentMethodCreate(
                  new RedirectPaymentMethodCreate()
                  {
                      Method = "ach",
                      Country = "US",
                      Currency = "USD",
                      RedirectUrl = "https://example.com/callback",
                  }
              ),
      }
  );
  ```

  ```go Go theme={"system"}
  amount := int64(1299)
  currency := "USD"
  country := "US"
  method := components.RedirectPaymentMethodCreateMethodAch
  redirectUrl := "https://example.com/callback"

  redirectPaymentMethodCreate := components.RedirectPaymentMethodCreate{
      Method:      method,
      Country:     country,
      Currency:    currency,
      RedirectURL: redirectUrl,
  }
  paymentMethod := components.CreateTransactionCreatePaymentMethodRedirectPaymentMethodCreate(redirectPaymentMethodCreate)

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

  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(RedirectPaymentMethodCreate.builder()
              .method(RedirectPaymentMethodCreateMethod.ACH)
              .country("US")
              .currency("USD")
              .redirectUrl("https://example.com/callback")
              .build()))
          .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 RedirectPaymentMethodCreate(
          method: 'ach',
          country: 'US',
          currency: 'USD',
          redirectUrl: 'https://example.com/callback'
      )
  );
  $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",
      payment_method={
          "method": "ach",
          "country": "US",
          "currency": "USD",
          "redirect_url": "https://example.com/callback",
      }
  )
  ```

  ```ts TypeScript theme={"system"}
  const transaction = await gr4vy.transactions.create({
      amount: 1299,
      currency: "USD",
      country: "US",
      intent: "capture",
      paymentMethod: {
          method: "ach",
          country: "US",
          currency: "USD",
          redirectUrl: "https://example.com/callback"
      }
  })
  ```
</CodeGroup>

After the transaction is created, the API response includes `payment_method.approval_url` and the `buyer_approval_pending` status.

```json theme={"system"}
{
  "type": "transaction",
  "id": "ea1efdd0-20f9-44d9-9b0b-0a3d71e9b625",
  "payment_method": {
    "type": "payment-method",
    "approval_url": "https://cdn.sandbox.spider.gr4vy.app/connectors/bluesnap/ach.html?token=..."
  },
  "method": "ach"
}
```

Open the `approval_url` in a popup so the buyer can enter their bank account and routing numbers in the BlueSnap-hosted form. After the buyer submits the form they are redirected to the `redirect_url` you provided. Do not rely solely on the redirect — either poll the transaction or (recommended) rely on webhooks to detect the final status.

## Recurring payments

ACH Direct Debit via BlueSnap supports storing the bank account for later reuse. Set `store: true` on the payment method when creating the transaction.

<CodeGroup>
  ```csharp C# theme={"system"}
  var transaction = await client.Transactions.CreateAsync(
      transactionCreate: new TransactionCreate()
      {
          Amount = 1299,
          Currency = "USD",
          Country = "US",
          Intent = "capture",
          PaymentMethod =
              TransactionCreatePaymentMethod.CreateRedirectPaymentMethodCreate(
                  new RedirectPaymentMethodCreate()
                  {
                      Method = "ach",
                      Country = "US",
                      Currency = "USD",
                      RedirectUrl = "https://example.com/callback",
                      Store = true,
                  }
              ),
      }
  );
  ```

  ```go Go theme={"system"}
  amount := int64(1299)
  currency := "USD"
  country := "US"
  method := components.RedirectPaymentMethodCreateMethodAch
  redirectUrl := "https://example.com/callback"
  store := true

  redirectPaymentMethodCreate := components.RedirectPaymentMethodCreate{
      Method:      method,
      Country:     country,
      Currency:    currency,
      RedirectURL: redirectUrl,
      Store:       &store,
  }
  paymentMethod := components.CreateTransactionCreatePaymentMethodRedirectPaymentMethodCreate(redirectPaymentMethodCreate)

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

  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(RedirectPaymentMethodCreate.builder()
              .method(RedirectPaymentMethodCreateMethod.ACH)
              .country("US")
              .currency("USD")
              .redirectUrl("https://example.com/callback")
              .store(true)
              .build()))
          .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 RedirectPaymentMethodCreate(
          method: 'ach',
          country: 'US',
          currency: 'USD',
          redirectUrl: 'https://example.com/callback',
          store: true
      )
  );
  $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",
      payment_method={
          "method": "ach",
          "country": "US",
          "currency": "USD",
          "redirect_url": "https://example.com/callback",
          "store": True,
      }
  )
  ```

  ```ts TypeScript theme={"system"}
  const transaction = await gr4vy.transactions.create({
      amount: 1299,
      currency: "USD",
      country: "US",
      intent: "capture",
      paymentMethod: {
          method: "ach",
          country: "US",
          currency: "USD",
          redirectUrl: "https://example.com/callback",
          store: true
      }
  })
  ```
</CodeGroup>

Gr4vy creates a BlueSnap vaulted shopper when the transaction is initiated. After the buyer fills in their bank details and the `CHARGE_PENDING` webhook is received, a stored payment method token is issued that can be used for subsequent payments without the buyer re-entering their bank details.

See [Recurring payments](/guides/features/recurring-payments/overview) for an overview of storing and reusing payment methods.

## Testing

BlueSnap provides test routing and account numbers for sandbox environments.

| Scenario                | Routing Number | Account Number |
| ----------------------- | -------------- | -------------- |
| Approved transaction    | `998877665`    | `223344556`    |
| Declined transaction    | `987654321`    | `123456789`    |
| Format validation error | `123456789`    | `123456789`    |

Use any US bank account holder name and address for the remaining fields.
