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

# Ryft - Card

> Connect to Ryft to accept credit and debit card payments, with sub-account routing and platform fees for marketplaces.

export const connector = {
  displayName: "Ryft",
  method: "card",
  features: "create_transaction deep_linking delayed_capture delete_token direct_capture direct_integration_create partial_capture partial_refunds payment_method_tokenization redirect_requires_popup refunds requires_webhook_setup three_d_secure_hosted transaction_sync verify_credentials void zero_auth",
  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",
  supportedCurrencies: "AED AUD CAD CHF CZK DKK EUR GBP HKD HUF NOK NZD PLN RON SEK TRY USD ZAR"
};

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>;
};

Ryft is a UK-based payment platform built for marketplaces and platforms. Alongside standard card processing, Ryft lets a platform take payments on behalf of its sub-accounts (for example sellers, hosts, or charities) and keep a platform fee on each payment.

## Setup

Ryft accounts are set up by the Ryft team. Contact [Ryft](https://www.ryftpay.com/) to open a sandbox and a production account, and to agree which card schemes and payment types your account needs.

### Enable American Express

American Express payments are disabled by default on Ryft accounts. Visa and Mastercard work without extra setup.

To accept American Express, enable the **Amex payments** capability in the Ryft portal under **Settings** -> **Capabilities**. For non-hosted sub-accounts, the main account requests the capability through the Ryft Accounts API instead. See the [Ryft capabilities documentation](https://developer.ryftpay.com/documentation/get_started/portal/settings#capabilities) for details.

## Credentials

When setting up Ryft in the dashboard, configure the following credentials. The API keys are in the **Developer** section of the Ryft portal, which you reach from the profile menu in the top right. See the [Ryft portal documentation](https://developer.ryftpay.com/documentation/get_started/portal) for details.

* **Public API Key** (`public_key`) - Your Ryft public key.
* **Secret API Key** (`secret_key`) - Your Ryft secret key. Treat this value as a password.
* **Webhook Secret** (`webhook_secret`, optional) - The secret for the webhook endpoint registered in Ryft. When set, Gr4vy validates the signature on every webhook Ryft sends.
* **Include platform fees in refunds** (`refund_platform_fee`, optional) - When enabled, every refund also refunds the platform fee on the payment. See [Platform fees](#platform-fees).

## Webhooks

Ryft reports the outcome of 3-D Secure challenges, captures, voids, and refunds by webhook, and Gr4vy uses those events to keep transactions in sync. Register a webhook endpoint for your Ryft account before you take live traffic.

1. Copy the webhook URL for your Ryft payment service, which is the `webhook_url` field on the payment service in the Gr4vy dashboard and API. Each payment service has its own URL.
2. In the Ryft portal, go to **Developer** -> **Webhooks** and create an endpoint with that URL.
3. Subscribe to the payment events, including the `PaymentSession` events and, if you use platform fees, the `PlatformFee` events.
4. Copy the secret for that endpoint into the **Webhook Secret** field on your Gr4vy connector.

For more details, see the [Ryft webhooks documentation](https://developer.ryftpay.com/documentation/get_started/webhooks).

## Connector configuration

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

* **Using Flow** - Configure Ryft as the target connector in [Flow](/guides/dashboard/flow/overview) to automatically route card transactions to this connector
* **Using the API** - Explicitly set the `payment_service_id` parameter to the Ryft 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" />

## Limitations

* **No digital wallets** - Apple Pay and Google Pay aren't supported through this connector.
* **No network tokens** - Stored cards are Ryft payment methods, and can't be used with another connector.
* **Single capture** - An authorization can be captured once, in full or for a lower amount. Multiple captures and captures above the authorized amount aren't supported.
* **Hosted 3-D Secure only** - Ryft runs its own 3-D Secure authentication. External 3-D Secure data can't be passed through.

## Integration

To accept card payments with Ryft, use one of Gr4vy's client-side integration methods to securely collect card details. Due to PCI compliance requirements, card data should never be sent directly to your servers.

You can integrate using:

* **[Embed](/guides/payments/embed/quick-start)** - A pre-built, customizable payment form that handles the complete payment flow
* **[Secure Fields](/guides/payments/secure-fields/quick-start)** - Embed card input fields for building custom payment forms while maintaining PCI compliance
* **[Mobile SDKs](/guides/get-started#client-side-and-mobile-sdks)** - Native SDKs for iOS, Android, React Native, and other platforms

These methods handle card data collection and tokenization. Once the card details are collected and tokenized, create a transaction through the Gr4vy API, which routes the payment to your configured Ryft connection based on your Flow rules or explicit `payment_service_id` parameter.

### 3-D Secure

When the card issuer asks for authentication, the transaction moves to `buyer_approval_pending` and returns an `approval_url`. Send the buyer to that URL, where Ryft runs the 3-D Secure challenge. Gr4vy then retrieves the result from Ryft and completes the transaction.

When you create the transaction through the API, set `redirect_url` on the `payment_method` so the buyer returns to your site after the challenge. Embed and the mobile SDKs handle this for you.

### Stored cards and recurring payments

Set `store` to `true` to save the card as a Gr4vy payment method. You can then use it for later customer-initiated payments, and for merchant-initiated subscription, installment, and unscheduled card-on-file payments. Set `payment_source` on every transaction in the series. For each later merchant-initiated payment, also set `merchant_initiated` and `is_subsequent_payment` to `true`. See [recurring payments](/guides/features/recurring-payments/overview) for the full set of flags for each scenario.

Ryft keeps each stored card on a Ryft customer record. Gr4vy looks up that record for you on every payment, so no extra fields are needed.

## Connection options

Use `connection_options` under the `ryft-card` key to route a payment to a Ryft sub-account and to take a platform fee on it. Both options are optional.

| Option | Type | Description |
| - | - | - |
| `sub_account_id` | string | The ID of a Ryft sub-account linked to your main account, for example `ac_123456789`. The payment is made on behalf of that sub-account. |
| `platform_fee` | integer | The fee your main account keeps from the payment, in the smallest currency unit (for example `205` for £2.05). Must be between `0` and `2000000`, and can't be more than the transaction amount, or Ryft rejects the payment. |

```json theme={"system"}
{
  "amount": 5000,
  "currency": "GBP",
  "country": "GB",
  "payment_method": {
    "method": "checkout-session",
    "id": "[CHECKOUT_SESSION_ID]"
  },
  "connection_options": {
    "ryft-card": {
      "sub_account_id": "ac_123456789",
      "platform_fee": 205
    }
  }
}
```

### Sub-accounts

When `sub_account_id` is set, Gr4vy sends every request for the transaction to Ryft on behalf of that sub-account. This includes 3-D Secure, captures, voids, refunds, and syncs, so you only need to send the option when you create the transaction.

### Platform fees

When `platform_fee` is set, Ryft moves the fee from the payment to your main account and settles the rest to the sub-account. For a delayed capture, Gr4vy applies the same fee when the transaction is captured, so there is no need to send it again. Platform fees only apply to payments made on behalf of a sub-account, so send `platform_fee` together with `sub_account_id`.

By default, a refund is taken from the sub-account's balance and the main account keeps its platform fee. To refund the platform fee as well, enable **Include platform fees in refunds** on the connector. The main account then bears the cost of the refunded fee. This setting applies to every refund on the connector, including partial refunds.

For more on how Ryft handles fees, see the [Ryft refunds documentation](https://developer.ryftpay.com/documentation/get_started/manage_payments/refund).

## Testing

Use the Ryft sandbox with the test cards from the [Ryft test cards documentation](https://developer.ryftpay.com/documentation/get_started/process_payments/test_cards). The page lists cards for 3-D Secure frictionless and challenge flows, and cards that trigger specific issuer declines.

To test American Express, enable the Amex capability on your sandbox account first, as described in [Enable American Express](#enable-american-express).
