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

# Report Payment Results (CSV)

> Upload the outcomes of your debits so Rightfoot can match and act on them

# Report Payment Results (CSV)

Upload a CSV of debit outcomes: which debits settled, which were returned, and why. Use it for debits Rightfoot cannot read from your payment processor itself, such as debits run from your LMS or through a processor you have not connected.

Each row is matched to a borrower and recorded as if Rightfoot had read it from your payment processor. Results you upload show up in [Retrieve Payment Results](/api-reference/payments/get-payment-events), and a return you report affects that borrower's monitoring.

## Key Information

<Card>
  <div>
    **File Size Limit:** 10 MB
  </div>

  <div>
    **Content-Type:** `multipart/form-data`, with the file in a part named `file`
  </div>

  <div>
    **Encoding:** UTF-8
  </div>

  <div>
    **Preview:** `dryRun=true` returns the full report and writes nothing
  </div>
</Card>

```bash theme={null}
curl -X POST "https://api.rightfoot.com/v1/payment_results/uploads?dryRun=true" \
  -H "Authorization: Bearer $RIGHTFOOT_API_KEY" \
  -F "file=@payment_results.csv"
```

## CSV Format

One row per debit. Headers are case-insensitive, and spaces and punctuation are read as underscores, so `Payment Date` is the same column as `payment_date`.

The file must have an amount, status, date and return code column, plus at least one of `authorizer_unique_id` or `reference`. A cell can be blank when it does not apply. For example, `return_code` and `returned_at` are blank on a settled debit.

| Column | Also accepted as | Required per row | Format |
| - | - | - | - |
| `authorizer_unique_id` | `authorizer_id` | One of these two | The identifier you [enrolled](/api-reference/payments/create-payment-enrollments) the borrower with |
| `reference` | `payment_reference` | One of these two | Your identifier for the debit. Strongly recommended — see [Re-sending](#re-sending-a-file) |
| `payment_amount` | `amount` | Yes | Dollars, e.g. `425.00` or `$1,234.56` |
| `payment_status` | `outcome`, `status` | Yes | See [Statuses](#statuses) |
| `submitted_at` | `payment_date`, `submitted` | Yes | When the debit was submitted. `YYYY-MM-DD`, `MM/DD/YYYY`, `MM/DD/YY`, `MM/DD/YYYY HH:MM` or ISO 8601. A date with no time zone is read as UTC |
| `settled_at` | `settlement_date`, `settled_date` | On `settled` rows | When the debit settled. Same formats as `submitted_at` |
| `returned_at` | `return_date`, `returned_date` | On `returned` and `late_return` rows | When the debit was returned. Same formats as `submitted_at` |
| `return_code` | `return_reason` | On `returned` and `late_return` rows | A NACHA R-code, see [Return codes](#return-codes) |
| `batch_id` | — | No | The `batch_id` of the balance check this debit was based on. Links the debit to that exact check |

Blank rows and summary rows (no identifier and no status, such as a totals line) are skipped. Cells containing `#N/A`, `N/A` or `-` are read as blank.

```csv theme={null}
authorizer_unique_id,reference,payment_amount,payment_status,submitted_at,settled_at,returned_at,return_code
1a2b3c4d,INV-90243,425.00,settled,01/14/2025,01/16/2025,,
5e6f7g8h,INV-90244,180.00,returned,01/14/2025,,01/17/2025,R01
```

<Tip>
  You can also upload a Rightfoot balance export with your own outcome columns added. Add `payment_status`, `payment_amount`, `payment_date`, `settled_at`, `returned_at` and `return_code` to the export and upload it as-is. Its own `Status` column holds the balance-check status, so your `payment_status` column takes precedence over it.
</Tip>

### Statuses

| Value | Also accepted | Meaning |
| - | - | - |
| `settled` | `paid` | Cleared — money received. Requires `settled_at` |
| `returned` | `rejected` | Bounced and never settled. Requires `returned_at` and `return_code` |
| `late_return` | `reversed` | Settled, then clawed back. Requires `returned_at` and `return_code`; `settled_at` is optional |
| `pending` | | Created but not yet submitted |
| `sent` | | Submitted and in flight |
| `voided` | | Cancelled before it went out |
| `invalid` | | Refused by the payment processor, so it never reached the bank |
| `refunded` | | Returned to the borrower |
| `not_attempted` | | Never sent |

Values are case-insensitive. A row with any other status is rejected and listed in `problems`.

### Return codes

On a `returned` or `late_return` row, `return_code` must be a NACHA R-code. `R01`, `r01`, `R1` and `R01 - Insufficient Funds` are all read as `R01`. The text `insufficient funds` is also read as `R01`, and `payment stopped` as `R08`. A return with any other text, a blank cell or `0` is rejected: without the code, Rightfoot cannot tell whether the borrower may be debited again.

A `settled` row must not have a return code. On other statuses, a code is stored as you sent it.

### Validation

Each row is checked on its own. A row that fails is rejected and every reason is listed in `problems`. The rest of the file is still recorded, so fix the rejected rows and upload them again. A row is rejected when:

* a required value is missing or can't be read: amount, status, `submitted_at`, an identifier, or the outcome date and return code its status requires
* `settled_at` or `returned_at` is earlier than `submitted_at`, or more than a day in the future
* a `late_return` has a `returned_at` earlier than its `settled_at`
* a `settled` row has a `return_code`

## What a reported return does

A return you report counts the same as one Rightfoot reads from your payment processor:

* **`R01` and `R09`** (insufficient or uncollected funds): the borrower stays eligible, and Rightfoot can retry the debit when there is enough in the account.
* **Any other return code** stops monitoring for that borrower. NACHA does not allow a debit to be re-presented after these codes without corrective action, such as a new authorization or a different account.

This applies only to rows matched to an enrolled borrower.

## Matching

A row with an `authorizer_unique_id` is matched to that borrower. A row with only a `reference` is matched when the reference identifies the borrower, for example when it contains their `authorizer_unique_id`. If a reference does not clearly identify one borrower, Rightfoot leaves the row unmatched rather than guessing.

An unmatched row is still stored. It is counted in `needsAttention` and listed in `problems` as a `warning`. To match it, upload the row again with the `authorizer_unique_id` filled in.

## Re-sending a file

Re-uploading is safe. Rows are keyed on `reference`, so the same debit is never recorded twice:

* A row whose status differs from the last one recorded for that `reference` records the new status. A debit can therefore go from `sent` to `settled` to `late_return` over successive files.
* A row identical to what is already recorded changes nothing. If Rightfoot recorded the status without a date, the row's `settled_at` or `returned_at` fills it in. A date already recorded is never changed.
* A row whose outcome date is earlier than the latest recorded outcome for its `reference` is ignored and listed as a warning, so an old export cannot overwrite newer results. A row with an undated status, such as `sent`, never replaces a settled or returned outcome.

Within one file, duplicate rows for the same `reference` are counted once. If two rows for the same `reference` disagree, all rows for that `reference` are rejected.

<Warning>
  **Send a `reference` on every row.** A row without one is keyed on borrower, amount and date. Two debits to the same borrower for the same amount on the same day would then be recorded as one.
</Warning>

## Reading the report

A file that can be parsed always returns `200`. Check the report rather than the status code:

* `dataRowCount` is the number of data rows found in the file. `rowCount` is how many of those were accepted, and `matched` and `needsAttention` split the accepted rows by whether a borrower was found.
* Each entry in `problems` has a `severity`. An `error` row was not recorded, and `warning` means the row was recorded but something was ignored, or it is unmatched. A row can have several `error` entries, one per issue. A `row` of `0` is a problem with the file as a whole, such as missing columns.

`400` is returned only when the file is rejected before any row is read: it is over 10 MB, or it is not UTF-8 text.


## OpenAPI

````yaml POST /v1/payment_results/uploads
openapi: 3.1.0
info:
  title: Rightfoot API
  description: >-
    Submit a batch of authorizers for balance checks, retrieve processed
    balances, and check the status of a batch.
  version: 0.3.0-alpha
servers:
  - url: https://api.rightfoot.com
security:
  - BearerAuth: []
tags:
  - name: General Availability
    description: |
      **General Availability** indicates that the endpoint is production-ready.
  - name: Early Access
    description: >
      **Early Access** indicates that the endpoint is available for early access
      customers and is being actively refined.
  - name: In Development
    description: >
      **In Development** indicates that the endpoint is currently being built
      and will be available in an upcoming release.
paths:
  /v1/payment_results/uploads:
    post:
      tags:
        - In Development
      summary: Report the outcomes of your debits via CSV upload
      description: >
        Upload a CSV of debit outcomes — settled, returned, and so on — for
        debits

        run outside Rightfoot, or from a payment processor Rightfoot does not
        read

        directly. Each row is matched to a borrower and recorded exactly as if

        Rightfoot had read it from your payment processor, so a return reported

        here feeds that borrower's monitoring.


        **File Size Limit:** 10 MB.


        **Content-Type:** `multipart/form-data` with the file in a part named
        `file`.


        **Encoding:** UTF-8 (a byte-order mark is fine).


        **Preview first.** `dryRun=true` parses and matches the whole file and

        returns the same report without writing anything.


        **Outcome dates are required.** A `settled` row needs `settled_at`. A

        `returned` or `late_return` row needs `returned_at` and a NACHA

        `return_code`. These dates are when the outcome happened, not when the
        debit

        was submitted.


        **Safe to re-send.** Rows are keyed on `reference`, so re-uploading a
        file

        never creates a payment twice. A row whose status differs from the last
        one

        recorded for its `reference` records the change; an identical row does

        nothing. A row whose outcome date is earlier than the latest recorded

        outcome never overwrites it.


        **Per-row results.** A file that parses always returns `200` with a
        row-level

        report. Invalid rows are rejected and listed in `problems`, one entry
        per

        issue, and the valid rows are still recorded.
      operationId: upload_payment_results
      parameters:
        - in: query
          name: dryRun
          schema:
            type: boolean
            default: false
          description: Validate and match the file without writing anything.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: >-
                    CSV of debit outcomes, one row per debit. See the column
                    reference on this page.
      responses:
        '200':
          description: >
            The file was processed. Check `needsAttention` and `problems`: a row
            can

            be stored but unmatched, or skipped entirely.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentResultsUploadResponse'
              example:
                format: generic
                rowCount: 2
                dataRowCount: 3
                matched: 1
                needsAttention: 1
                dryRun: false
                rows:
                  - row: 2
                    reference: INV-90243
                    authorizerUniqueId: 1a2b3c4d
                    status: settled
                    amountCents: 42500
                  - row: 3
                    reference: INV-90244
                    authorizerUniqueId: null
                    status: returned
                    amountCents: 18000
                problems:
                  - row: 3
                    reason: No matching authorizer for reference 'INV-90244'
                    severity: warning
                  - row: 4
                    reason: Missing returned_at — required when status is returned
                    severity: error
                  - row: 4
                    reason: Missing return_code — required when status is returned
                    severity: error
        '400':
          description: >
            Bad Request — the upload was rejected before any row was read: the
            file

            is over 10 MB, or is not UTF-8 text.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              example:
                status: error
                status_code: 400
                error:
                  code: BAD_REQUEST
                  message: File exceeds the 10 MB upload limit.
                  timestamp: '2025-01-02T17:32:21Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
components:
  schemas:
    PaymentResultsUploadResponse:
      type: object
      properties:
        format:
          type: string
          enum:
            - generic
            - annotated_export
          description: >
            How the file was read. `annotated_export` is a Rightfoot balance
            export

            with your outcome columns added; anything else is `generic`.
        rowCount:
          type: integer
          description: Rows accepted for matching and recording.
        dataRowCount:
          type: integer
          description: Data rows found in the file, excluding blank and summary rows.
        matched:
          type: integer
          description: Accepted rows matched to a borrower.
        needsAttention:
          type: integer
          description: >
            Accepted rows Rightfoot could not match to a borrower. They are
            stored,

            and re-uploading them with an `authorizer_unique_id` matches them.
        dryRun:
          type: boolean
          description: Echoes the `dryRun` parameter. When true, nothing was written.
        rows:
          type: array
          description: One entry per accepted row.
          items:
            type: object
            properties:
              row:
                type: integer
                description: The row's line in your file (the header is row 1).
              reference:
                type: string
                description: |
                  The row's `reference`. For a row sent without one, the key
                  Rightfoot assigned it.
              authorizerUniqueId:
                type:
                  - string
                  - 'null'
                description: The borrower the row was matched to, or null when unmatched.
              status:
                type: string
                description: The status recorded, in Rightfoot's vocabulary.
              amountCents:
                type: integer
                description: The debit amount in cents.
        problems:
          type: array
          description: >
            Everything that needs your attention. An `error` row was not
            recorded; a

            `warning` row was recorded, but something was ignored or it is
            unmatched.

            `row` is `0` for a problem with the file as a whole.
          items:
            type: object
            properties:
              row:
                type: integer
              reason:
                type: string
              severity:
                type: string
                enum:
                  - error
                  - warning
    ApiErrorResponse:
      type: object
      properties:
        status:
          type: string
          description: Status of the error, either "error" or "success"
        status_code:
          type: integer
          description: HTTP status code of the error
        error:
          type: object
          properties:
            code:
              type: string
              description: Error code (e.g., "RESOURCE_NOT_FOUND")
            message:
              type: string
              description: Detailed error message.
            details:
              type: string
              description: Additional details about the error
            timestamp:
              type: string
              format: date-time
              description: Time the error occurred
            suggestion:
              type: string
              description: Suggested action to resolve the error
        documentation_url:
          type: string
          format: uri
          description: URL to the documentation for the error
  responses:
    Unauthorized:
      description: Unauthorized
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
          example:
            status: error
            status_code: 401
            error:
              code: UNAUTHORIZED
              message: Invalid API key provided.
              timestamp: '2024-09-16T12:01:00Z'
              suggestion: Please check your API key.
            documentation_url: https://api.rightfoot.com/docs
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: >
        Authentication to the API is performed via Bearer Token Authentication.
        Provide your API key as the bearer token in the Authorization header.


        All API requests must be made over HTTPS. Calls made over plain HTTP
        will fail. API requests without authentication will also fail.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.