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

# PayPal - Venmo

> Configure Venmo via PayPal as a payment method in Gr4vy.

export const connector = {
  displayName: "Venmo",
  method: "venmo",
  features: "create_session create_transaction delayed_capture delete_token direct_capture direct_integration_create partial_capture partial_refunds payment_method_tokenization redirect_requires_popup refunds requires_webhook_setup settlement_reporting transaction_sync verify_credentials void",
  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_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 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>;
};

Venmo is a digital wallet and payments app owned by PayPal that allows buyers to send and receive money and to pay online retailers. It is available to buyers in the United States and is widely used by mobile-first shoppers.

This connector accepts Venmo through a PayPal Commerce Platform account, using the same credentials as the [PayPal](./paypal-paypal) wallet connector. To accept Venmo through Braintree instead, see [Venmo via Braintree](./braintree-venmo).

<Warning>Venmo is only available for United States merchants, in USD, to buyers in the United States.</Warning>

<Note>
  Venmo is a redirect payment method, so [Embed](/guides/payments/embed/quick-start/overview) presents it alongside your other methods and handles the approval popup for you. To integrate directly, see [Integration](#integration).
</Note>

## Setup

Follow the [PayPal setup instructions](./paypal) before configuring Venmo payments, then ask PayPal to enable Venmo on your PayPal merchant account.

The connector takes the same **Client ID** and **Client secret** as the PayPal wallet connector. You can configure both connectors on the same credentials.

## Supported countries

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

## Supported currencies

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

## Capabilities

<ConnectorCapabilities data={connector} />

## Integration

If you use [Embed](/guides/payments/embed/quick-start/overview), Venmo needs no integration work. Embed treats it as a redirect payment method, opens the approval page in a popup, and returns the buyer to your checkout.

To integrate directly, set `integration_client` on the transaction to one of the following.

* **`redirect`**: Send the buyer to the `approval_url` Gr4vy returns, and let the Gr4vy-hosted page run PayPal's SDK. See [Redirect to the Gr4vy-hosted approval page](#redirect-to-the-gr4vy-hosted-approval-page).
* **`web`**: Render the Venmo button inside your own checkout with the PayPal Web SDK v6. See [Web: run the SDK yourself](#web-run-the-sdk-yourself).
* **`ios` and `android`**: Hand the transaction session to PayPal's mobile SDK. See [iOS and Android](#ios-and-android).

### Create the transaction

Every flow starts with the same request: `method` set to `venmo`, an `integration_client`, and a `redirect_url` your app can handle. Keep this call server-side. Set the transaction `intent` to match the intent configured on your connection.

The samples below use `web`. Substitute the `integration_client` for the route you are integrating.

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

  ```go Go theme={"system"}
  amount := int64(1299)
  currency := "USD"
  country := "US"
  integrationClient := "web"
  intent := components.TransactionIntentCapture
  method := components.RedirectPaymentMethodCreateMethodVenmo
  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,
    IntegrationClient: &integrationClient,
    Intent:            &intent,
    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")
      .integrationClient("web")
      .intent(TransactionIntent.CAPTURE)
      .paymentMethod(TransactionCreatePaymentMethod.of(RedirectPaymentMethodCreate.builder()
        .method(RedirectPaymentMethodCreateMethod.VENMO)
        .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',
    integrationClient: 'web',
    intent: 'capture',
    paymentMethod: new RedirectPaymentMethodCreate(
      method: 'venmo',
      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",
    integration_client="web",
    intent="capture",
    payment_method={
      "method": "venmo",
      "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",
    integrationClient: "web",
    intent: "capture",
    paymentMethod: {
      method: "venmo",
      country: "US",
      currency: "USD",
      redirectUrl: "https://example.com/callback"
    }
  })
  ```
</CodeGroup>

The response has a `status` of `buyer_approval_pending`, a `session_token`, and a `payment_method.approval_url`.

```json theme={"system"}
{
  "type": "transaction",
  "id": "ea1efdd0-20f9-44d9-9b0b-0a3d71e9b625",
  "status": "buyer_approval_pending",
  "payment_method": {
    "type": "payment-method",
    "method": "venmo",
    "mode": "redirect",
    "approval_url": "https://cdn.sandbox.example.gr4vy.app/connectors/paypal/venmo.html?token=..."
  },
  "method": "venmo"
}
```

The transaction stays `buyer_approval_pending` until the buyer completes approval and your app calls the completion URL. If the Venmo flow fails before that, the transaction remains pending and no error is recorded against it, so do not treat a pending transaction as a failed one. Use [webhooks](/guides/features/webhooks/overview) to follow the final status rather than relying on the buyer returning.

### Redirect to the Gr4vy-hosted approval page

Use this to offer Venmo without adding the PayPal JS SDK to your checkout. Create the transaction with `integration_client` set to `redirect`, then send the buyer to the returned `payment_method.approval_url`. That page loads the SDK and starts the Venmo flow as soon as it opens, then returns the buyer to your `redirect_url`.

Open it in a popup rather than replacing the page. Venmo's flow opens its own window, and this connection is marked as requiring a popup.

### Web: run the SDK yourself

Use this to put a Venmo button inside your own checkout. It uses the [PayPal Web SDK v6](https://developer.paypal.com/sdk/js/v6/), not the older Smart Buttons SDK.

<Warning>
  Do not load the Smart Buttons SDK (`www.paypal.com/sdk/js`) and the Web SDK v6 on the same page. The v6 script does not claim the `window.paypal` global when the older SDK already holds it, and attaches itself to `window.paypal.v6` instead. The result is a Venmo button that silently never appears. If your checkout already uses Smart Buttons for PayPal, either move PayPal to v6 as well or load v6 under its own `data-namespace`.
</Warning>

1. On page load, fetch the connection's standalone session to get its PayPal `clientId`. This needs the `transactions.write` scope, so call it from your server. It creates no transaction and makes no call to PayPal.

```sh theme={"system"}
POST /payment-service-definitions/paypal-venmo/sessions
```

```json theme={"system"}
{
  "type": "payment-service-session",
  "status": "succeeded",
  "response_body": {
    "clientId": "AY-client-id",
    "merchantId": "BN-merchant-id"
  }
}
```

<Note>
  This call is optional. The `clientId` is a static connection value, so if you already have it you can pass it straight to the SDK. The session endpoint exists so you don't have to hard-code or separately distribute the connection's credentials to your frontend. `merchantId` is returned for multi-party setups; the Gr4vy-hosted page does not send one.
</Note>

2. Load the v6 core script and create an SDK instance scoped to the `venmo-payments` component, then render the SDK's `<venmo-button>`.

```html theme={"system"}
<script async src="https://www.sandbox.paypal.com/web-sdk/v6/core"></script>
<venmo-button type="pay" hidden></venmo-button>
```

```js theme={"system"}
// clientId comes from the standalone session in step 1.
const sdkInstance = await window.paypal.createInstance({
  clientId,
  clientMetadataId: crypto.randomUUID(),
  components: ["venmo-payments"],
  pageType: "checkout",
});

const paymentSession = sdkInstance.createVenmoOneTimePaymentSession({
  onApprove: () => window.location.assign(defaultCompletionUrl),
  onCancel: () => { /* the buyer backed out */ },
  onError: (error) => console.error(error),
});

document
  .querySelector("venmo-button")
  .addEventListener("venmo-click", () =>
    paymentSession.start({ presentationMode: "auto" }, createOrder())
  );
```

`presentationMode: "auto"` lets the SDK choose how to present the flow: an in-page modal, a popup window, or the Venmo app. Pass `"modal"` or `"popup"` to fix it.

3. Create the transaction (see above) inside `createOrder`, then use its `session_token` to get the [session data](/reference/transactions/get-transaction-session). This returns the PayPal `orderId` and a `default_completion_url`. It is meant to be called from the frontend and is not exposed in the SDKs, so call it with a plain request authenticated by the `session_token`.

```sh theme={"system"}
POST /transactions/:transaction_id/session?token=:session_token
```

```json theme={"system"}
{
  "type": "transaction-session",
  "session_data": {
    "clientId": "AY-client-id",
    "orderId": "5O190127TN364715T",
    "returnUrl": "https://api.example.gr4vy.app/transactions/approve/..."
  },
  "default_completion_url": "https://api.example.gr4vy.app/transactions/approve/...",
  "integration_client": "web"
}
```

`start()` takes a *promise* of `{ orderId }`, so return one from `createOrder` and the flow opens on the buyer's click while the order is still being created. Create the order in response to the click rather than ahead of time, so it is fresh when the flow opens.

4. When `onApprove` fires, send the buyer to the `default_completion_url`. Gr4vy finalizes the order with PayPal and then returns the buyer to the `redirect_url` you set on the transaction.

### iOS and Android

Set `integration_client` to `ios` or `android` and use an app deep link (for example `yourapp://`) as the `redirect_url`. The transaction session returns the same `session_data` as the web flow — `clientId`, `orderId` and `returnUrl` — which you hand to PayPal's mobile SDK to collect the buyer's approval. On approval, call the `default_completion_url` to finalize the order.

## About tokenization

Pass `store: true` on a transaction to vault the buyer's Venmo account for future use, together with a buyer so the stored account can be found again. Subsequent payments then charge the stored token without sending the buyer back to Venmo.

## Testing

PayPal's Venmo sandbox is limited and is built around buyers in the United States. Expect it to be unreliable, and budget time for retries.

* **Create a sandbox Venmo buyer** at [account.ext.live.venmo.com/signup](https://account.ext.live.venmo.com/signup). A successful sign-up does not guarantee a successful payment: an account that authenticates can still fail to complete one.
* **Set both sandbox accounts to the United States** — the business account and the personal account. If either is set elsewhere, the Venmo button does not render at all.
* **No Venmo app is needed.** The sandbox offers a web login on desktop and on mobile web.
* **Pass `sandboxSupport: { enabled: true }`** to `start()` outside production when you run the SDK yourself. The Gr4vy-hosted approval page already does this.
* **`testBuyerCountry: "US"`** on `createInstance` declares a United States buyer. It is sandbox-only and rejected in production, and the Gr4vy-hosted page does not send it.

<Warning>
  PayPal can disregard sandbox traffic that originates outside the United States, including traffic behind a VPN. A flow that reaches Venmo's login page can still fail immediately afterwards, on Venmo's own domain, leaving the Gr4vy transaction at `buyer_approval_pending` with no error recorded against it. If you see that, the failure is inside Venmo's hosted flow and not in your integration.
</Warning>

<Note>
  PayPal has acknowledged that Venmo's sandbox has limited functionality, including for buyers inside the United States, and that they are working to improve it.
</Note>
