Skip to content

Generate a loan tape for a single investor

Overview

A loan tape is a scheduled CSV export that delivers loan or payment data to an investor over SFTP. When an investor owns only part of your portfolio, you scope a tape to that investor so its file contains only the loans associated with them. This runbook walks an integration engineer through configuring an investor-filtered tape, generating it, and confirming that the file was produced and delivered.

The filter is set once, when you create the tape: a tape carries a single investorId, and every time that tape generates, Peach includes only the loans currently associated with that investor.

This page covers the procedure end to end — from the prerequisite loan-investor association through confirming a delivered file. It does not cover:

Before you begin

Prerequisites

  • An investor record exists for your company, and you have its investorId (format IR-XXXX-XXXX). Retrieve it from the Admin Portal; the programmatic investor-list endpoint is Peach-internal, not part of the public API.

  • Each loan that should appear on the tape is associated with that investor. A loan joins the tape only through an active LoanInvestorAssociation; loans with no association to the tape's investor are excluded. Set the association with PUT /api/people/{personId}/loans/{loanId}/investors:

    PUT /api/people/{personId}/loans/{loanId}/investors
    Content-Type: application/json
    
    {
      "effectiveDate": "2026-05-01",
      "investors": [
        {
          "investorId": "IR-7F3A-9K2D",
          "share": 1.0
        }
      ]
    }

    investorId and share are required per investor; share is the ownership fraction in decimal form, where 1.0 is 100% and 0.15 is 15%. This share is what the tape reports as investorOwnershipPercentage.

  • A loan tape type is configured, or you create one in Step 1. The tape type defines which fields the CSV carries, its money unit, and its reporting rules.

  • An SFTP destination is reachable, and a GPG public key is available if you encrypt the file. These credentials are supplied by the receiving investor or your own delivery team — Peach does not generate them.

  • Permissions on your API credential: loan.investors:update, loan.tape.type:create, loan.tape:create, loan.tape:generate, and loan.tape.record:list.

Assumed knowledge

Key concepts

  • Loan tape type — the reusable template that defines a tape's fields, money unit, and reporting rules. Many tapes can share one type.
  • Loan tape — a single delivery configuration: one tape type plus one SFTP destination and, optionally, one investorId filter.
  • Investor association — the LoanInvestorAssociation linking a loan to an investor with an ownership share; it is what the investor filter joins against.
  • Snapshot dependency — each tape row is built from the loan's snapshot for the report date; missing snapshots are handled by the tape type's actionOnMissingData setting.
  • Asynchronous generation — generating a tape fires a background job and returns immediately with an operationId; the file is produced and uploaded by a worker, and you observe completion through the tape's records.

The procedure

The diagram below shows the full sequence: the prerequisite association, configuring the tape type and the investor-filtered tape, generating asynchronously, and monitoring the records.

Tape workerPeach APILenderTape workerPeach APILenderPrerequisite — associate loans with the investorConfigureGenerate (asynchronous)MonitorPUT /people/{personId}/loans/{loanId}/investors204 (LoanInvestorAssociation set)POST /companies/{companyId}/loan-tape-types201 loanTapeTypeId (TY-...)POST /companies/{companyId}/loan-tapes (investorId set)201 loanTapeId (TA-...)POST /companies/{companyId}/loan-tapes/generate (reportDates)201 [{ loanTapeId, operationId, reportDate }]fire LoanTapeCreateFileEventjoin loans on LoanInvestorAssociation, build CSV, upload to SFTPGET /companies/{companyId}/loan-tapes/{loanTapeId}/recordsrecords with status (null → created → succeeded / failed)
Tape workerPeach APILenderTape workerPeach APILenderPrerequisite — associate loans with the investorConfigureGenerate (asynchronous)MonitorPUT /people/{personId}/loans/{loanId}/investors204 (LoanInvestorAssociation set)POST /companies/{companyId}/loan-tape-types201 loanTapeTypeId (TY-...)POST /companies/{companyId}/loan-tapes (investorId set)201 loanTapeId (TA-...)POST /companies/{companyId}/loan-tapes/generate (reportDates)201 [{ loanTapeId, operationId, reportDate }]fire LoanTapeCreateFileEventjoin loans on LoanInvestorAssociation, build CSV, upload to SFTPGET /companies/{companyId}/loan-tapes/{loanTapeId}/recordsrecords with status (null → created → succeeded / failed)

Step 1: Create or select the loan tape type

A tape type fixes the column set and reporting rules. Create one if you don't already have a suitable type, or list existing types with GET /api/companies/{companyId}/loan-tape-types and reuse one.

POST /api/companies/{companyId}/loan-tape-types
Content-Type: application/json

{
  "tapeName": "Investor portfolio — loans",
  "tapeType": "loans",
  "fields": [
    "reportDate",
    "loanId",
    "investorId",
    "investorOwnershipPercentage",
    "loanStatus",
    "outstandingBalanceTotalAmount"
  ],
  "moneyUnit": "subUnit100",
  "reportDaysWhenPaidOff": 10
}

tapeType, tapeName, and fields are required. Amounts default to subUnit100 (integer pennies); set moneyUnit to fullUnit for decimal dollars. reportDaysWhenPaidOff has a minimum of 10, and any lower value is raised to 10.

For the complete field catalog and per-field meanings, see the Loan tape field reference — do not enumerate all fields here.

Expected result: The response returns 201 with the new loanTapeTypeId (format TY-XXXX-XXXX). Save it for Step 2.

Step 2: Create the investor-filtered loan tape

Create the tape that ties the tape type to a delivery destination, and set investorId to scope it to one investor. This is the switch that turns a portfolio-wide tape into an investor-filtered one.

POST /api/companies/{companyId}/loan-tapes
Content-Type: application/json

{
  "loanTapeTypeId": "TY-4M8X-2P9Q",
  "investorId": "IR-7F3A-9K2D",
  "sftpHostname": "sftp.example-investor.com",
  "sftpAccessMethod": "usernamePassword",
  "sftpUsername": "<your-sftp-username>",
  "sftpPassword": "<your-sftp-password>"
}

loanTapeTypeId and sftpHostname are required. When sftpAccessMethod is usernamePassword (the default), supply sftpUsername and sftpPassword. To encrypt the file, set encryptionMethod to GPG and provide the investor's publicKey.

Never paste real credentials

sftpPassword, sftpUsername, and publicKey are real request fields that carry secrets supplied by the receiving investor. Use a secret manager and clearly-marked placeholders in any shared example. Never commit a literal password or key to a repository, ticket, or document.

Leaving investorId unset produces a tape covering every loan for the company; setting it restricts the tape to that one investor for the life of the tape.

Expected result: The response returns 201 with the new loanTapeId (format TA-XXXX-XXXX) and echoes the investorId you set. Save the loanTapeId for Step 3 and for monitoring.

Execute

Generate the tape for one or more report dates. Generation is asynchronous: the call validates the request, fires one background job per tape and report date, and returns immediately. It does not return the file.

POST /api/companies/{companyId}/loan-tapes/generate
Content-Type: application/json

{
  "loanTapeIds": ["TA-DJEK-CVDE"],
  "reportDates": ["2026-05-31"],
  "forceReplace": false,
  "uploadFile": true
}

reportDates is required (ISO YYYY-MM-DD). If you omit loanTapeIds, Peach generates every tape configured for the company. forceReplace (default false) creates a new record even when one already exists for that tape and date; uploadFile (default true) controls whether the worker uploads the file to SFTP after building it.

Expected result: The response returns 201 with one entry per tape and date, each carrying a loanTapeId, the reportDate, and an operationId you can use to trace the background job.

{
  "data": [
    {
      "loanTapeId": "TA-DJEK-CVDE",
      "reportDate": "2026-05-31",
      "operationId": "8c1f0e6a-2d4b-4f9a-9b27-1a2b3c4d5e6f"
    }
  ]
}

After completion

Validate

Poll the tape's records to track the job and confirm delivery. Records are returned sorted by createdAt descending, so the most recent generation is first.

GET /api/companies/{companyId}/loan-tapes/{loanTapeId}/records

Read the record's status to determine where the job is:

statusMeaning
nullGeneration is in progress — the record was created when the job triggered and has not reached a terminal state.
createdThe file was built but has not yet been uploaded to SFTP.
succeededThe tape was generated and, if configured, uploaded.
failedGeneration failed; read failureDescription for the reason.

The status moves through these states as the worker runs:

generate fired (status null)

file built, awaiting SFTP upload

file built, upload skipped (uploadFile=false)

uploaded to SFTP

see failureDescription

upload failed

InProgress

Created

Succeeded

Failed

generate fired (status null)

file built, awaiting SFTP upload

file built, upload skipped (uploadFile=false)

uploaded to SFTP

see failureDescription

upload failed

InProgress

Created

Succeeded

Failed

When status is succeeded, confirm:

  • tapeFileName is populated and matches the file delivered to the SFTP destination.
  • The record's investorId matches the investor you scoped the tape to.
  • The file contains only loans associated with that investor. A loan that should appear but is missing almost always lacks an active LoanInvestorAssociation to this investor.

System behavior after generation

  • Generation runs in a worker. The API call only triggers the job. The worker resolves the loan set, builds the CSV, and uploads it; you observe progress through the records, not the generate response.
  • Loan inclusion is status-scoped. A loan is included when its status is Active, Accelerated, Frozen, or ChargedOff, or when it is PaidOff and was paid off within the tape type's reportDaysWhenPaidOff window.
  • Missing snapshot data is governed by actionOnMissingData. The effective default is includeIDsAndEmptyData: a loan with no snapshot for the report date is still reported, with only its identifier columns populated and dataStatus set to missingData. With excludeFromTape, such loans are dropped from the file entirely.
  • Line-of-credit draws can be reported as separate rows when the tape type sets shouldReportDrawsSeparately; otherwise an LOC is reported on an aggregated level.

Known limitations

  • The investor filter depends on the association being present and not soft-deleted. Known limitation. The worker joins loans to the investor through LoanInvestorAssociation and excludes associations with a deleted_at value. A loan removed from the investor, or never associated, silently does not appear — the tape does not warn you.
  • Generation is asynchronous. By design. The generate call returns operationIds, not a file. Treat the records status as the source of truth for completion.
  • One tape filters to one investor. By design. investorId is a single, nullable value on the tape. To deliver to several investors, create one tape per investor; leave investorId blank only when you want every company loan on one file.
  • excludeFromTape shifts validation onto you. By design. When a tape type uses excludeFromTape, loans with missing data are dropped without notice, so you are responsible for reconciling the loan count against your own records.
  • Membership-fee columns are gated. On the roadmap. The *MembershipFeesAmount tape fields populate only when the membership-fee feature is enabled.

If it fails

  • status is failed. Read failureDescription on the record. A common cause is an SFTP problem — an unreachable host, a rejected credential, or a missing target directory. Correct the tape's SFTP settings (or the destination), then regenerate.
  • The tape is empty or missing expected loans. The loans most likely have no active LoanInvestorAssociation to this investor, or they fall outside the included statuses, or they have no snapshot for the report date and the tape type uses excludeFromTape. Verify the associations first, then the loan statuses, then the snapshots.
  • A record already exists for that tape and date. Re-running generate without forceReplace will not overwrite an existing record. Set forceReplace: true to create a fresh record for the same date.
  • Generation never reaches a terminal state. A status that stays null for an extended period points to a stalled worker job; use the operationId from the generate response when contacting Peach support.

Troubleshooting

Why does the tape include fewer loans than I expect? A loan appears only if it has an active association to the tape's investor and falls within the included statuses and (under excludeFromTape) has a snapshot for the report date. Check those three conditions in that order.

I generated the tape but no file arrived at the SFTP destination. Check the record status. If it is created, the file was built but the upload has not completed or failed; if it is failed, read failureDescription. Confirm you generated with uploadFile: true — false builds the record without delivering a file.

Can I change which investor a tape reports? The investorId is part of the tape's configuration. Update the tape, or create a new tape for the other investor. The association data itself is changed through PUT /api/people/{personId}/loans/{loanId}/investors.

The amounts look off by a factor of 100. Amounts default to subUnit100 (pennies). Set the tape type's moneyUnit to fullUnit for decimal dollars.

API quick reference

ActionMethodEndpoint
Associate loan with investorPUT/api/people/{personId}/loans/{loanId}/investors
Create loan tape typePOST/api/companies/{companyId}/loan-tape-types
List loan tape typesGET/api/companies/{companyId}/loan-tape-types
Create loan tape (set investorId)POST/api/companies/{companyId}/loan-tapes
Generate loan tapesPOST/api/companies/{companyId}/loan-tapes/generate
List loan tape recordsGET/api/companies/{companyId}/loan-tapes/{loanTapeId}/records

For full field contracts, see the Loan Tapes API, Loan Tape Types API, and Loan investors API.

See also