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

# Tapi Cash

> Connect to Tapi Cash to accept cash payments in Latin America.

export const connector = {
  displayName: "Tapi Cash",
  method: "tapi",
  features: "create_transaction custom_approval_expiration direct_capture refunds requires_webhook_setup verify_credentials",
  supportedCountries: "AR CO MX PE",
  supportedCurrencies: "ARS COP MXN PEN 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>;
};

Tapi Cash is a cash payment method by Mattilda that allows buyers in Latin America to pay using cash vouchers. Tapi uses a redirect flow where the buyer receives payment instructions.

<Note>
  This connector is a custom integration for Mattilda and is only available to enrolled users. Contact Mattilda directly to begin the onboarding process.
</Note>

## Setup

Contact the Tapi account manager to obtain credentials.

## Credentials

When setting up Tapi Cash in the dashboard, configure the following credentials:

* **Username** - The Tapi username.
* **Password** - The Tapi password.
* **Login API key** - The API key for the `/login` endpoint.
* **References API key** - The API key used to create and retrieve reference data via the `/reference` endpoint.
* **Approval URL** - The custom frontend URL provided by Mattilda.
* **Tapi Modality ID** - The identifier of the payment modality used in the transaction.
* **Tapi company code** - The company code associated with the operation.
* **Tapi identifier name** (optional) - The identifier name associated with the operation.

## Capabilities

<ConnectorCapabilities data={connector} />

## Supported countries

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

## Supported currencies

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

## Webhooks

Tapi requires manual webhook setup. Contact Mattilda to request the webhook URL and configure it for your account.

Tapi sends the following webhook events:

* **confirmed** - The payment was successfully captured.
* **failed** - The payment could not be processed or was rejected.
* **reverse** - A retraction of a previous capture event. This indicates the capture did not actually complete on Tapi's side. Mattilda handles the reversal by triggering a full refund to align the transaction state.

<Note>
  A reversal is not a real refund at the PSP level. It marks the transaction as reversed in alignment with Tapi's state.
</Note>

## Integration

The default integration for Tapi Cash uses a redirect flow.

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 = "MXN",
      Country = "MX",
      PaymentMethod =
        TransactionCreatePaymentMethod.CreateRedirectPaymentMethodCreate(
          new RedirectPaymentMethodCreate()
          {
            Method = "tapi",
            Country = "MX",
            Currency = "MXN",
            RedirectUrl = "https://example.com/callback",
          }
        ),
    }
  );
  ```

  ```go Go theme={"system"}
  amount := int64(1299)
  currency := "MXN"
  country := "MX"
  method := components.RedirectPaymentMethodCreateMethodTapi
  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,
    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("MXN")
      .country("MX")
      .paymentMethod(TransactionCreatePaymentMethod.of(RedirectPaymentMethodCreate.builder()
        .method(RedirectPaymentMethodCreateMethod.TAPI)
        .country("MX")
        .currency("MXN")
        .redirectUrl("https://example.com/callback")
        .build()))
      .build())
    .call();

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

  ```php PHP theme={"system"}
  $transactionCreate = new TransactionCreate(
    amount: 1299,
    currency: 'MXN',
    country: 'MX',
    paymentMethod: new RedirectPaymentMethodCreate(
      method: 'tapi',
      country: 'MX',
      currency: 'MXN',
      redirectUrl: 'https://example.com/callback'
    )
  );
  $response = $client->transactions->create($transactionCreate);
  $transaction = $response->transaction;
  ```

  ```python Python theme={"system"}
  transaction: models.Transaction = client.transactions.create(
    amount=1299,
    currency="MXN",
    country="MX",
    payment_method={
      "method": "tapi",
      "country": "MX",
      "currency": "MXN",
      "redirect_url": "https://example.com/callback",
    }
  )
  ```

  ```ts TypeScript theme={"system"}
  const transaction = await client.transactions.create({
    amount: 1299,
    currency: "MXN",
    country: "MX",
    paymentMethod: {
      method: "tapi",
      country: "MX",
      currency: "MXN",
      redirectUrl: "https://example.com/callback"
    }
  })
  ```
</CodeGroup>

After the transaction is created, the status is set to `processing`. The payment reference expires after 7 days.

```json theme={"system"}
{
  "type": "transaction",
  "id": "ea1efdd0-20f9-44d9-9b0b-0a3d71e9b625",
  "payment_method": {
    "type": "payment-method",
    "approval_url": "https://cdn.gr4vy.com/connectors/..."
  },
  "method": "tapi"
}
```

Redirect the buyer to the `approval_url` where they receive payment instructions. Once payment is confirmed through a webhook, the transaction progresses to a `capture_succeeded` state.

## Custom expiration date

You can control when a Tapi payment reference expires using two approaches:

* **Top-level `approval_expires_at`**: Set the expiration directly on the transaction request using an ISO 8601 datetime value.
* **`connection_options.payment_method_expires_at`**: Override the expiration at the connector level using a `YYYY-MM-DD` date string.

When both are provided, `connection_options.payment_method_expires_at` takes priority. If neither is set, the reference expires after 7 days.

To use the top-level field, include `approval_expires_at` in your transaction request.

<CodeGroup>
  ```csharp C# highlight={6} theme={"system"}
  var transaction = await client.Transactions.CreateAsync(
    transactionCreate: new TransactionCreate()
    {
      Amount = 1299,
      Currency = "MXN",
      ApprovalExpiresAt = DateTimeOffset.Parse("2026-04-10T23:59:59Z"),
      PaymentMethod =
        TransactionCreatePaymentMethod.CreateRedirectPaymentMethodCreate(
          new RedirectPaymentMethodCreate()
          {
            Method = "tapi",
            Country = "MX",
            Currency = "MXN",
            RedirectUrl = "https://example.com/callback",
          }
        ),
    }
  );
  ```

  ```go Go highlight={6} theme={"system"}
  transactionCreate := components.TransactionCreate{
    Amount:            amount,
    Currency:          currency,
    Country:           &country,
    PaymentMethod:     &paymentMethod,
    ApprovalExpiresAt: gr4vy.Time(time.Date(2026, 4, 10, 23, 59, 59, 0, time.UTC)),
  }

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

  ```java Java highlight={5} theme={"system"}
  CreateTransactionResponse transactionResponse = gr4vyClient.transactions().create()
    .transactionCreate(TransactionCreate.builder()
      .amount(1299L)
      .currency("MXN")
      .approvalExpiresAt(OffsetDateTime.parse("2026-04-10T23:59:59Z"))
      .paymentMethod(TransactionCreatePaymentMethod.of(RedirectPaymentMethodCreate.builder()
        .method(RedirectPaymentMethodCreateMethod.TAPI)
        .country("MX")
        .currency("MXN")
        .redirectUrl("https://example.com/callback")
        .build()))
      .build())
    .call();
  ```

  ```php PHP highlight={5} theme={"system"}
  $transactionCreate = new TransactionCreate(
    amount: 1299,
    currency: 'MXN',
    country: 'MX',
    approvalExpiresAt: new \DateTime('2026-04-10T23:59:59Z'),
    paymentMethod: new RedirectPaymentMethodCreate(
      method: 'tapi',
      country: 'MX',
      currency: 'MXN',
      redirectUrl: 'https://example.com/callback'
    )
  );
  $response = $client->transactions->create($transactionCreate);
  ```

  ```python Python highlight={5} theme={"system"}
  transaction: models.Transaction = client.transactions.create(
    amount=1299,
    currency="MXN",
    country="MX",
    approval_expires_at=datetime(2026, 4, 10, 23, 59, 59, tzinfo=timezone.utc),
    payment_method={
      "method": "tapi",
      "country": "MX",
      "currency": "MXN",
      "redirect_url": "https://example.com/callback",
    }
  )
  ```

  ```ts TypeScript highlight={5} theme={"system"}
  const transaction = await client.transactions.create({
    amount: 1299,
    currency: "MXN",
    country: "MX",
    approvalExpiresAt: new Date("2026-04-10T23:59:59Z"),
    paymentMethod: {
      method: "tapi",
      country: "MX",
      currency: "MXN",
      redirectUrl: "https://example.com/callback"
    }
  })
  ```
</CodeGroup>

## Connection options

The Tapi connector also supports passing a payment expiration date via `connection_options`. This takes priority over the top-level `approval_expires_at` field.

<CodeGroup>
  ```csharp C# highlight={6-12} theme={"system"}
  var transaction = await client.Transactions.CreateAsync(
    transactionCreate: new TransactionCreate()
    {
      Amount = 1299,
      Currency = "MXN",
      ConnectionOptions = new TransactionCreateConnectionOptions()
      {
        MattildaTapi = new ConnectionOptionsMattildaTapi()
        {
          PaymentMethodExpiresAt = "2026-03-27",
        },
      },
      PaymentMethod =
        TransactionCreatePaymentMethod.CreateRedirectPaymentMethodCreate(
          new RedirectPaymentMethodCreate()
          {
            Method = "tapi",
            Country = "MX",
            Currency = "MXN",
            RedirectUrl = "https://example.com/callback",
          }
        ),
    }
  );
  ```

  ```go Go highlight={1-5,12} theme={"system"}
  connectionOptions := components.TransactionCreateConnectionOptions{
    MattildaTapi: &components.ConnectionOptionsMattildaTapi{
      PaymentMethodExpiresAt: gr4vy.String("2026-03-27"),
    },
  }

  transactionCreate := components.TransactionCreate{
    Amount:            amount,
    Currency:          currency,
    Country:           &country,
    ConnectionOptions: &connectionOptions,
    PaymentMethod:     &paymentMethod,
  }

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

  ```java Java highlight={5-9} theme={"system"}
  CreateTransactionResponse transactionResponse = gr4vyClient.transactions().create()
    .transactionCreate(TransactionCreate.builder()
      .amount(1299L)
      .currency("MXN")
      .connectionOptions(TransactionCreateConnectionOptions.builder()
        .mattildaTapi(ConnectionOptionsMattildaTapi.builder()
          .paymentMethodExpiresAt("2026-03-27")
          .build())
        .build())
      .paymentMethod(TransactionCreatePaymentMethod.of(RedirectPaymentMethodCreate.builder()
        .method(RedirectPaymentMethodCreateMethod.TAPI)
        .country("MX")
        .currency("MXN")
        .redirectUrl("https://example.com/callback")
        .build()))
      .build())
    .call();
  ```

  ```php PHP highlight={5-9} theme={"system"}
  $transactionCreate = new TransactionCreate(
    amount: 1299,
    currency: 'MXN',
    country: 'MX',
    connectionOptions: new TransactionCreateConnectionOptions(
      mattildaTapi: new ConnectionOptionsMattildaTapi(
        paymentMethodExpiresAt: '2026-03-27'
      )
    ),
    paymentMethod: new RedirectPaymentMethodCreate(
      method: 'tapi',
      country: 'MX',
      currency: 'MXN',
      redirectUrl: 'https://example.com/callback'
    )
  );
  $response = $client->transactions->create($transactionCreate);
  ```

  ```python Python highlight={5-9} theme={"system"}
  transaction: models.Transaction = client.transactions.create(
    amount=1299,
    currency="MXN",
    country="MX",
    connection_options={
      "mattilda-tapi": {
        "payment_method_expires_at": "2026-03-27",
      },
    },
    payment_method={
      "method": "tapi",
      "country": "MX",
      "currency": "MXN",
      "redirect_url": "https://example.com/callback",
    }
  )
  ```

  ```ts TypeScript highlight={5-9} theme={"system"}
  const transaction = await client.transactions.create({
    amount: 1299,
    currency: "MXN",
    country: "MX",
    connectionOptions: {
      "mattilda-tapi": {
        paymentMethodExpiresAt: "2026-03-27",
      },
    },
    paymentMethod: {
      method: "tapi",
      country: "MX",
      currency: "MXN",
      redirectUrl: "https://example.com/callback"
    }
  })
  ```
</CodeGroup>

The `payment_method_expires_at` field must be in ISO 8601 format (`YYYY-MM-DD`). If not specified, it defaults to 7 days from the transaction creation date.
