# Classify Your Own Transactions

When your application already has access to users' transaction data, Sail can help identify which transactions qualify as tax-deductible healthcare expenses under [IRS Section 213(d)](https://www.irs.gov/publications/p502).

In this guide, you'll create an external connection, push a batch of transactions, and read back the classification results.

## Requirements

Before you start, make sure you have:

- A [Sail user](/docs/get-started/get-started-overview/create-a-user) created for your internal user
- An [API key](/docs/get-started/get-started-overview/api-key) with the `expenses` and `ingest` scopes
- A [webhook endpoint](/docs/get-started/get-started-overview/webhooks) configured to receive events

## Step 1: Create an external connection

Before you can push transactions, you need to [Create an External Connection](/reference/connections/post-users-user-id-connections) to group the transaction data and label its source. Set `type` to `external` and choose a `provider_label` that identifies where this data comes from (for example, "Plaid feed" or "Internal ledger").

<Tabs>
  <Tab title="Request">
    ```bash
    curl -X POST https://live.savewithsail.com/api/v1/users/usr_abc123/connections \
      -H "Authorization: Bearer sk_live_..." \
      -H "Content-Type: application/json" \
      -d '{
        "type": "external",
        "provider_label": "Acme Budgeting · Plaid feed"
      }'
    ```
  </Tab>

  <Tab title="Response">
    ```json
    {
      "id": "conn_9f2c",
      "user_id": "usr_abc123",
      "type": "external",
      "status": "active",
      "products": ["expenses"],
      "accounts": [],
      "created_at": "2026-08-10T14:32:00Z"
    }
    ```
  </Tab>
</Tabs>

The response confirms the connection was created. Save the `id` (`conn_9f2c` in this example). You need it to push transactions in the next step. The `products` array shows that this connection supports `expenses`, which means Sail will classify the transactions you push to it.

## Step 2: Push transactions

Send a batch of transactions to the external connection. Sail accepts up to 500 transactions per request. Each transaction needs an `external_transaction_id` that's unique within this connection. If you push the same `external_transaction_id` again, Sail updates the existing record and re-classifies it.

<Tabs>
  <Tab title="Request">
    ```bash
    curl -X POST https://live.savewithsail.com/api/v1/users/usr_abc123/connections/conn_9f2c/transactions \
      -H "Authorization: Bearer sk_live_..." \
      -H "Content-Type: application/json" \
      -d '{
        "transactions": [
          {
            "external_transaction_id": "tx-1001",
            "date": "2026-08-05",
            "amount": 27.02,
            "description": "WALGREENS #4821 RX",
            "merchant_name": "Walgreens",
            "mcc": "5912"
          },
          {
            "external_transaction_id": "tx-1002",
            "date": "2026-08-06",
            "amount": 85.00,
            "description": "AMAZON.COM*AB1CD2EF3",
            "merchant_name": "Amazon",
            "mcc": null
          }
        ]
      }'
    ```
  </Tab>

  <Tab title="Response">
    ```json
    {
      "status": "processing",
      "job_id": "job_x",
      "accepted": 2,
      "duplicates_updated": 0
    }
    ```
  </Tab>
</Tabs>

The response returns `202` with a `job_id`. Classification happens asynchronously. Don't poll for results. Instead, listen for the `transactions.ingested` webhook.

### Listen for the webhook

After you push a batch, Sail enriches and classifies each transaction. When the batch is done, Sail sends a `transactions.ingested` webhook:

```json
{
  "id": "evt_7f3k9m",
  "event": "transactions.ingested",
  "created_at": "2026-08-10T14:33:15Z",
  "user_id": "usr_abc123",
  "data": {
    "connection_id": "conn_9f2c",
    "job_id": "job_x",
    "created": 2,
    "updated": 0
  }
}
```

The `created` and `updated` counts show how many expenses were new versus re-classified from a duplicate push.

You will receive an `expenses.verified` event when 213d classification has finished for these expenses.

## Step 3: Pull classification results

Once you receive the webhook, [fetch the expenses](/reference/expenses/get-users-user-id-expenses) to see how Sail classified each transaction.

<Tabs>
  <Tab title="Request">
    ```bash
    curl https://live.savewithsail.com/api/v1/users/usr_abc123/expenses \
      -H "Authorization: Bearer sk_live_..."
    ```
  </Tab>

  <Tab title="Response">
    ```json
    {
      "data": [
        {
          "id": "exp_9f2c",
          "connection_id": "conn_9f2c",
          "external_transaction_id": "tx-1001",
          "name": "Walgreens",
          "amount": 27.02,
          "status": "eligible",
          "source_type": "external",
          "origin": {
            "type": "transaction",
            "detail": "WALGREENS #4821 RX"
          },
          "merchant": {
            "name": "Walgreens",
            "supported_merchant_id": "mer_walgreens"
          },
          "enrichment": {
            "category": "Pharmacies",
            "subcategory": "Drug Stores"
          },
          "section_213d_classification": {
            "status": "eligible",
            "category": "Prescription Medications",
            "reasoning": "Pharmacy purchase classified as eligible under Section 213(d)."
          }
        }
        // ... additional expenses
      ],
      "pagination": {
        "limit": 25,
        "offset": 0,
        "total": 2
      }
    }
    ```
  </Tab>
</Tabs>

You can [filter the results](/reference/expenses/get-users-user-id-expenses) by status, connection, or source type to get specific subsets:

```bash
# Get only eligible expenses
curl "https://live.savewithsail.com/api/v1/users/usr_abc123/expenses?status=eligible" \
  -H "Authorization: Bearer sk_live_..."

# Get only expenses from a specific connection
curl "https://live.savewithsail.com/api/v1/users/usr_abc123/expenses?connection_id=conn_9f2c" \
  -H "Authorization: Bearer sk_live_..."
```

The `section_213d_classification.status` field tells you the outcome for each transaction:

| Status                 | What it means                                                                                                   | What to do                                                                                                                                                                                                   |
| :--------------------- | :-------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eligible`             | The expense qualifies as tax-deductible under Section 213(d).                                                   | No action needed.                                                                                                                                                                                            |
| `ineligible`           | The expense does not qualify.                                                                                   | No action needed.                                                                                                                                                                                            |
| `itemization_required` | Sail can't classify the transaction without seeing individual items (for example, a mixed-basket Amazon order). | Prompt the user to connect the merchant via a [connect widget session](/reference/connect-account-widgets/post-hosted-sessions) for item-level data, or upload a receipt via the [receipt upload](/reference/expenses/post-users-user-id-expenses-expense-id-receipt) endpoint. |
| `lmn_required`         | The item could be eligible with a Letter of Medical Necessity (for example, a massage chair).                   | Prompt the user to provide a letter from their healthcare provider.                                                                                                                                          |

The top-level `status` field groups these into three categories: `eligible`, `ineligible`, and `needs_review` (which covers both `itemization_required` and `lmn_required`).

<Callout type="info">
  **Note**

  Check the `merchant.supported_merchant_id` field in the expense response. When it is non-null for a `needs_review` expense, Sail supports a store connection for that merchant. Prompt the user to connect their merchant account through the Connect widget to retrieve item-level data and classify individual items. See [Enrich transactions with item-level data](/docs/guides/enrich-transactions-with-item-level-data) for details.
</Callout>