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

# Reporting and exporting payment data

> Search, filter, and export Straddle payment data using the API and dashboard for reporting, reconciliation, and accounting workflows in your platform.

The payments search API provides powerful capabilities to query, filter, and analyze both charges and payouts in a unified interface. With over 20 search parameters and support for complex filtering, you can build everything from real-time dashboards to detailed reconciliation reports that match your exact business requirements.

## What You'll Build With This Guide

This guide equips you with patterns and code examples for:

* **Unified payment searches**: Query charges and payouts together or separately
* **Advanced filtering**: Combine date ranges, amounts, statuses, and metadata for precise results
* **Reconciliation workflows**: Match funding events to bank statements using the funding event ID from the ACH addenda record
* **Return tracking**: Analyze reversed payments and identify high-risk patterns
* **Export capabilities**: Generate CSV and JSON reports for accounting systems
* **Performance monitoring**: Build real-time metrics and anomaly detection
* **Pagination strategies**: Efficiently process large datasets with parallel requests

Whether you need daily settlement reports, customer payment histories, or failure analysis, this guide provides production-ready patterns for every reporting scenario.

## Unified Payment Search

The payments search endpoint allows you to query both charges and payouts together or separately, providing a complete view of money movement through your platform.

### Basic Search

Search for all payments in a date range:

```bash theme={null}
curl -X GET "https://sandbox.straddle.com/v1/payments/search?\
  created_from=2024-10-01&\
  created_to=2024-10-31" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Response includes both charges and payouts:

```json theme={null}
{
  "data": [
    {
      "id": "ch_abc123",
      "payment_type": "charge",
      "amount": 10000,
      "status": "paid",
      "created_at": "2024-10-15T10:00:00Z"
    },
    {
      "id": "po_def456",
      "payment_type": "payout",
      "amount": 25000,
      "status": "paid",
      "created_at": "2024-10-16T14:00:00Z"
    }
  ],
  "pagination": {
    "current_page": 1,
    "total_pages": 5,
    "total_count": 247
  }
}
```

## Search Parameters

### Date Filters

| Parameter           | Description                  | Format     | Example      |
| ------------------- | ---------------------------- | ---------- | ------------ |
| `created_from`      | Start of creation date range | YYYY-MM-DD | `2024-10-01` |
| `created_to`        | End of creation date range   | YYYY-MM-DD | `2024-10-31` |
| `updated_from`      | Start of update date range   | YYYY-MM-DD | `2024-10-01` |
| `updated_to`        | End of update date range     | YYYY-MM-DD | `2024-10-31` |
| `payment_date_from` | Start of payment date range  | YYYY-MM-DD | `2024-10-01` |
| `payment_date_to`   | End of payment date range    | YYYY-MM-DD | `2024-10-31` |

### Payment Filters

| Parameter       | Description              | Values                                                                                  | Example              |
| --------------- | ------------------------ | --------------------------------------------------------------------------------------- | -------------------- |
| `payment_type`  | Filter by type           | `charge`, `payout`                                                                      | `charge`             |
| `status`        | Filter by status         | `created`, `scheduled`, `pending`, `paid`, `failed`, `reversed`, `cancelled`, `on_hold` | `paid`               |
| `status_reason` | Filter by failure reason | `insufficient_funds`, `closed_bank_account`, etc.                                       | `insufficient_funds` |
| `amount_min`    | Minimum amount in cents  | Integer                                                                                 | `10000`              |
| `amount_max`    | Maximum amount in cents  | Integer                                                                                 | `500000`             |

### Reference Filters

| Parameter      | Description         | Example           |
| -------------- | ------------------- | ----------------- |
| `paykey`       | Filter by paykey    | `pk_abc123xyz`    |
| `external_id`  | Your reference ID   | `inv_12345`       |
| `customer_id`  | Customer identifier | `cust_789`        |
| `payment_id`   | Specific payment ID | `ch_def456`       |
| `trace_number` | ACH trace number    | `071000301234567` |

### Pagination & Sorting

| Parameter    | Description      | Default      | Values                                               |
| ------------ | ---------------- | ------------ | ---------------------------------------------------- |
| `page`       | Page number      | `1`          | Integer                                              |
| `limit`      | Results per page | `100`        | 1-500                                                |
| `sort_by`    | Sort field       | `created_at` | `created_at`, `updated_at`, `amount`, `payment_date` |
| `sort_order` | Sort direction   | `desc`       | `asc`, `desc`                                        |

## Common Search Patterns

### Daily Settlement Report

Get all successful payments for settlement reconciliation:

```javascript theme={null}
async function getDailySettlement(date) {
  const payments = await straddle.payments.search({
    payment_date_from: date,
    payment_date_to: date,
    status: 'paid',
    sort_by: 'payment_type'
  });
  
  const summary = {
    date,
    charges: {
      count: 0,
      amount: 0
    },
    payouts: {
      count: 0,
      amount: 0
    },
    net: 0
  };
  
  for (const payment of payments.data) {
    if (payment.payment_type === 'charge') {
      summary.charges.count++;
      summary.charges.amount += payment.amount;
    } else {
      summary.payouts.count++;
      summary.payouts.amount += payment.amount;
    }
  }
  
  summary.net = summary.charges.amount - summary.payouts.amount;
  
  return summary;
}
```

### Failed Payment Analysis

Identify failure patterns for risk optimization:

```javascript theme={null}
async function analyzeFailures(startDate, endDate) {
  const failures = await straddle.payments.search({
    created_from: startDate,
    created_to: endDate,
    status: 'failed',
    limit: 500
  });
  
  // Group by failure reason
  const reasonAnalysis = {};
  
  for (const payment of failures.data) {
    const reason = payment.status_details.reason;
    
    if (!reasonAnalysis[reason]) {
      reasonAnalysis[reason] = {
        count: 0,
        total_amount: 0,
        payment_types: { charge: 0, payout: 0 }
      };
    }
    
    reasonAnalysis[reason].count++;
    reasonAnalysis[reason].total_amount += payment.amount;
    reasonAnalysis[reason].payment_types[payment.payment_type]++;
  }
  
  return {
    period: { from: startDate, to: endDate },
    total_failures: failures.pagination.total_count,
    by_reason: reasonAnalysis
  };
}
```

### Customer Payment History

View all payments for a specific customer:

```javascript theme={null}
async function getCustomerPaymentHistory(customerId, paykey) {
  const payments = await straddle.payments.search({
    paykey: paykey,
    sort_by: 'created_at',
    sort_order: 'desc',
    limit: 100
  });
  
  return {
    customer_id: customerId,
    paykey: paykey,
    total_payments: payments.pagination.total_count,
    recent_payments: payments.data.map(p => ({
      id: p.id,
      type: p.payment_type,
      amount: p.amount,
      status: p.status,
      date: p.created_at,
      description: p.description
    }))
  };
}
```

## Advanced Filtering

### Multi-Status Search

Find payments in specific states:

```bash theme={null}
curl -X GET "https://api.straddle.com/v1/payments/search?\
  status=pending&\
  status=scheduled&\
  payment_type=charge" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Amount Range Filtering

Find high-value transactions:

```bash theme={null}
curl -X GET "https://api.straddle.com/v1/payments/search?\
  amount_min=100000&\
  amount_max=1000000&\
  created_from=2024-10-01" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Metadata Search

Search using your custom metadata:

```javascript theme={null}
async function searchByMetadata(key, value) {
  const payments = await straddle.payments.search({
    [`metadata.${key}`]: value
  });
  
  return payments.data;
}

// Example: Find all payments for a specific order
const orderPayments = await searchByMetadata('order_id', 'ord_789');
```

## Reconciliation Workflows

### Bank Statement Reconciliation

Match funding events to bank transactions using the funding event `id`. Each funding event produces a single ACH transfer; the ACH addenda record on that transfer contains the funding event `id` and a description (e.g., `charge deposit`, `payout withdrawal`). Your bank surfaces this in the transaction detail, giving you a direct, programmatic link back to the Straddle API.

```javascript theme={null}
async function reconcileBankStatement(statementDate, bankTransactions) {
  // Get funding events created around the statement date.
  // Each entry in bankTransactions is expected to include an `addendaId` field
  // parsed from the ACH addenda record on the bank statement.
  // Note: the API filters by created_at, not transfer_date. Funding events are
  // typically created one business day before the transfer_date, so widen the
  // range slightly if your statement date falls on a Monday or after a holiday.
  const fundingEvents = await straddle.fundingEvents.list({
    created_from: statementDate,
    created_to: statementDate
  });

  const reconciliation = {
    date: statementDate,
    matched: [],
    unmatched_funding: [],
    unmatched_bank: [...bankTransactions]
  };

  // Primary match: funding event id from the ACH addenda record.
  for (const event of fundingEvents.data) {
    const bankIndex = reconciliation.unmatched_bank.findIndex(
      t => t.addendaId === event.id
    );

    if (bankIndex !== -1) {
      reconciliation.matched.push({
        funding_event_id: event.id,
        event_type: event.event_type,
        bank_transaction: reconciliation.unmatched_bank[bankIndex],
        amount: event.amount
      });
      reconciliation.unmatched_bank.splice(bankIndex, 1);
    } else {
      // Fallback: match by trace number if the bank does not surface the addenda id.
      const traceIndex = reconciliation.unmatched_bank.findIndex(
        t => t.trace === event.trace_number
      );
      if (traceIndex !== -1) {
        reconciliation.matched.push({
          funding_event_id: event.id,
          event_type: event.event_type,
          bank_transaction: reconciliation.unmatched_bank[traceIndex],
          amount: event.amount,
          matched_by: 'trace_number'
        });
        reconciliation.unmatched_bank.splice(traceIndex, 1);
      } else {
        reconciliation.unmatched_funding.push(event);
      }
    }
  }

  return reconciliation;
}
```

### Return Tracking

Monitor and analyze returned payments:

```javascript theme={null}
async function trackReturns(startDate, endDate) {
  // Get all reversed payments
  const reversed = await straddle.payments.search({
    created_from: startDate,
    created_to: endDate,
    status: 'reversed'
  });
  
  const returnAnalysis = {
    period: { from: startDate, to: endDate },
    total_returns: reversed.pagination.total_count,
    by_code: {},
    by_day: {},
    high_risk_paykeys: []
  };
  
  // Analyze return patterns
  for (const payment of reversed.data) {
    const code = payment.status_details.code;
    const date = payment.updated_at.split('T')[0];
    
    // Track by return code
    if (!returnAnalysis.by_code[code]) {
      returnAnalysis.by_code[code] = { count: 0, amount: 0 };
    }
    returnAnalysis.by_code[code].count++;
    returnAnalysis.by_code[code].amount += payment.amount;
    
    // Track by day
    if (!returnAnalysis.by_day[date]) {
      returnAnalysis.by_day[date] = { count: 0, amount: 0 };
    }
    returnAnalysis.by_day[date].count++;
    returnAnalysis.by_day[date].amount += payment.amount;
  }
  
  // Identify high-risk paykeys
  const paykeyReturns = {};
  for (const payment of reversed.data) {
    if (!paykeyReturns[payment.paykey]) {
      paykeyReturns[payment.paykey] = 0;
    }
    paykeyReturns[payment.paykey]++;
  }
  
  returnAnalysis.high_risk_paykeys = Object.entries(paykeyReturns)
    .filter(([_, count]) => count > 2)
    .map(([paykey, count]) => ({ paykey, return_count: count }))
    .sort((a, b) => b.return_count - a.return_count);
  
  return returnAnalysis;
}
```

## Export Patterns

### CSV Export

Generate CSV reports for accounting:

```javascript theme={null}
async function exportPaymentsCSV(startDate, endDate) {
  const payments = await straddle.payments.search({
    created_from: startDate,
    created_to: endDate,
    limit: 500  // Process in batches for large exports
  });
  
  const csv = [
    // Headers
    'ID,Type,Amount,Status,Created,Updated,Description,External ID,Trace Number'
  ];
  
  for (const payment of payments.data) {
    csv.push([
      payment.id,
      payment.payment_type,
      payment.amount / 100,  // Convert to dollars
      payment.status,
      payment.created_at,
      payment.updated_at,
      `"${payment.description}"`,  // Quote for commas
      payment.external_id || '',
      payment.trace_number || ''
    ].join(','));
  }
  
  return csv.join('\n');
}
```

### JSON Export with Details

Export complete payment data:

```javascript theme={null}
async function exportDetailedPayments(filters) {
  const allPayments = [];
  let page = 1;
  let hasMore = true;
  
  while (hasMore) {
    const result = await straddle.payments.search({
      ...filters,
      page,
      limit: 100
    });
    
    // Enrich with full details
    for (const payment of result.data) {
      const detailed = payment.payment_type === 'charge'
        ? await straddle.charges.retrieve(payment.id)
        : await straddle.payouts.retrieve(payment.id);
      
      allPayments.push(detailed);
    }
    
    hasMore = page < result.pagination.total_pages;
    page++;
  }
  
  return {
    export_date: new Date().toISOString(),
    filters,
    total_count: allPayments.length,
    payments: allPayments
  };
}
```

## Monitoring & Alerts

### Real-Time Monitoring

Track payment metrics in real-time:

```javascript theme={null}
async function getPaymentMetrics() {
  const today = new Date().toISOString().split('T')[0];
  
  // Today's activity
  const todayPayments = await straddle.payments.search({
    created_from: today,
    created_to: today
  });
  
  // Pending payments
  const pending = await straddle.payments.search({
    status: 'pending'
  });
  
  // Recent failures
  const failures = await straddle.payments.search({
    status: 'failed',
    created_from: today,
    created_to: today
  });
  
  return {
    timestamp: new Date().toISOString(),
    today: {
      total_count: todayPayments.pagination.total_count,
      total_volume: todayPayments.data.reduce((sum, p) => sum + p.amount, 0)
    },
    pending: {
      count: pending.pagination.total_count,
      volume: pending.data.reduce((sum, p) => sum + p.amount, 0)
    },
    failures: {
      count: failures.pagination.total_count,
      primary_reason: getMostCommonReason(failures.data)
    }
  };
}
```

### Anomaly Detection

Identify unusual payment patterns:

```javascript theme={null}
async function detectAnomalies(lookbackDays = 7) {
  const endDate = new Date();
  const startDate = new Date();
  startDate.setDate(startDate.getDate() - lookbackDays);
  
  const payments = await straddle.payments.search({
    created_from: startDate.toISOString().split('T')[0],
    created_to: endDate.toISOString().split('T')[0]
  });
  
  const anomalies = [];
  
  // Large transaction detection
  const avgAmount = payments.data.reduce((sum, p) => sum + p.amount, 0) / payments.data.length;
  const largeTransactions = payments.data.filter(p => p.amount > avgAmount * 5);
  
  if (largeTransactions.length > 0) {
    anomalies.push({
      type: 'large_transactions',
      count: largeTransactions.length,
      payments: largeTransactions.map(p => p.id)
    });
  }
  
  // Velocity check - too many payments from same paykey
  const paykeyVelocity = {};
  for (const payment of payments.data) {
    if (!paykeyVelocity[payment.paykey]) {
      paykeyVelocity[payment.paykey] = [];
    }
    paykeyVelocity[payment.paykey].push(payment);
  }
  
  for (const [paykey, payments] of Object.entries(paykeyVelocity)) {
    if (payments.length > 10) {  // More than 10 payments in period
      anomalies.push({
        type: 'high_velocity',
        paykey,
        count: payments.length,
        total_amount: payments.reduce((sum, p) => sum + p.amount, 0)
      });
    }
  }
  
  // Unusual failure rate
  const failureRate = payments.data.filter(p => p.status === 'failed').length / payments.data.length;
  if (failureRate > 0.15) {  // More than 15% failure rate
    anomalies.push({
      type: 'high_failure_rate',
      rate: failureRate,
      message: `${(failureRate * 100).toFixed(1)}% failure rate detected`
    });
  }
  
  return anomalies;
}
```

## Pagination Strategies

### Cursor-Based Iteration

Process large result sets efficiently:

```javascript theme={null}
async function* iterateAllPayments(filters) {
  let page = 1;
  let hasMore = true;
  
  while (hasMore) {
    const result = await straddle.payments.search({
      ...filters,
      page,
      limit: 100
    });
    
    for (const payment of result.data) {
      yield payment;
    }
    
    hasMore = page < result.pagination.total_pages;
    page++;
    
    // Rate limiting
    await new Promise(resolve => setTimeout(resolve, 100));
  }
}

// Usage
async function processAllPayments() {
  for await (const payment of iterateAllPayments({ status: 'paid' })) {
    await processPayment(payment);
  }
}
```

### Parallel Processing

Speed up large exports with parallel requests:

```javascript theme={null}
async function parallelExport(filters) {
  // First, get total count
  const initial = await straddle.payments.search({
    ...filters,
    limit: 1
  });
  
  const totalPages = initial.pagination.total_pages;
  const batchSize = 5;  // Process 5 pages at a time
  
  const allPayments = [];
  
  for (let i = 1; i <= totalPages; i += batchSize) {
    const pagePromises = [];
    
    for (let page = i; page < Math.min(i + batchSize, totalPages + 1); page++) {
      pagePromises.push(
        straddle.payments.search({
          ...filters,
          page,
          limit: 100
        })
      );
    }
    
    const results = await Promise.all(pagePromises);
    
    for (const result of results) {
      allPayments.push(...result.data);
    }
  }
  
  return allPayments;
}
```

## Performance Optimization

### Efficient Date Ranges

<Tip>
  For better performance, use specific date ranges rather than open-ended queries. The API performs best with date ranges under 90 days.
</Tip>

```javascript theme={null}
// Good - Specific date range
const efficient = await straddle.payments.search({
  created_from: '2024-10-01',
  created_to: '2024-10-31'
});

// Less efficient - Open-ended
const lessEfficient = await straddle.payments.search({
  created_from: '2024-01-01'
  // No end date
});
```

### Index-Friendly Filters

Use indexed fields for faster queries:

```javascript theme={null}
// Fast - Uses indexed fields
const fast = await straddle.payments.search({
  payment_id: 'ch_abc123',  // Direct ID lookup
  paykey: 'pk_xyz789',       // Indexed field
  status: 'paid'             // Indexed enum
});

// Slower - Requires scanning
const slower = await straddle.payments.search({
  'metadata.custom_field': 'value'  // Not indexed
});
```

## Common Use Cases

### Monthly Billing Report

```javascript theme={null}
async function generateMonthlyReport(year, month) {
  const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
  const endDate = new Date(year, month, 0).toISOString().split('T')[0];
  
  const report = {
    period: { year, month },
    summary: {
      total_charges: 0,
      total_payouts: 0,
      net_revenue: 0,
      success_rate: 0
    },
    daily_breakdown: {},
    top_customers: [],
    failure_analysis: {}
  };
  
  // Get all payments for the month
  const payments = await straddle.payments.search({
    created_from: startDate,
    created_to: endDate,
    limit: 500
  });
  
  // Process metrics
  let successful = 0;
  const customerTotals = {};
  
  for (const payment of payments.data) {
    // Summary totals
    if (payment.payment_type === 'charge') {
      report.summary.total_charges += payment.amount;
    } else {
      report.summary.total_payouts += payment.amount;
    }
    
    // Success tracking
    if (payment.status === 'paid') {
      successful++;
    }
    
    // Daily breakdown
    const day = payment.created_at.split('T')[0];
    if (!report.daily_breakdown[day]) {
      report.daily_breakdown[day] = {
        charges: 0,
        payouts: 0,
        count: 0
      };
    }
    report.daily_breakdown[day].count++;
    if (payment.payment_type === 'charge') {
      report.daily_breakdown[day].charges += payment.amount;
    } else {
      report.daily_breakdown[day].payouts += payment.amount;
    }
    
    // Track by customer
    if (!customerTotals[payment.paykey]) {
      customerTotals[payment.paykey] = 0;
    }
    customerTotals[payment.paykey] += payment.amount;
  }
  
  // Calculate final metrics
  report.summary.net_revenue = report.summary.total_charges - report.summary.total_payouts;
  report.summary.success_rate = (successful / payments.data.length) * 100;
  
  // Top customers
  report.top_customers = Object.entries(customerTotals)
    .sort(([,a], [,b]) => b - a)
    .slice(0, 10)
    .map(([paykey, total]) => ({ paykey, total }));
  
  return report;
}
```

## Best Practices

<CardGroup cols={2}>
  <Card title="Use Date Ranges" icon="calendar">
    Always specify date ranges to improve query performance
  </Card>

  <Card title="Paginate Large Results" icon="list-ol">
    Use pagination for exports exceeding 100 records
  </Card>

  <Card title="Cache Frequently Used" icon="database">
    Cache search results that don't change frequently
  </Card>

  <Card title="Monitor Rate Limits" icon="gauge">
    Implement rate limiting for bulk operations
  </Card>
</CardGroup>

## Troubleshooting

### Missing Payments

If payments aren't appearing in search results:

1. **Check date ranges** - Ensure dates include the payment creation time
2. **Verify filters** - Remove filters one by one to identify issues
3. **Check status** - Some statuses like `unknown` may not appear by default
4. **Review pagination** - Check if results are on subsequent pages

### Performance Issues

For slow queries:

* Narrow date ranges to 30 days or less
* Use specific IDs when possible
* Avoid complex metadata queries
* Implement caching for repeated searches

## Next Steps

<CardGroup cols={2}>
  <Card title="API Reference" icon="code" href="/api-reference/payments/search">
    Complete search endpoint documentation
  </Card>

  <Card title="Webhook Events" icon="webhook" href="/webhooks/overview/events">
    Real-time payment notifications
  </Card>

  <Card title="Funding Search" icon="download" href="/api-reference/funding/search">
    Search and analyze funding events
  </Card>

  <Card title="Funding Events" icon="building-columns" href="/guides/payments/funding">
    Settlement and reconciliation
  </Card>
</CardGroup>
