Skip to content
Last updated

Line of Credit Migration Guide


Introduction

This guide provides end-to-end instructions on how to migrate an existing line of credit (LOC) portfolio from your legacy system to Peach, i.e., preparing your data, creating borrowers and loans, populating historical records, executing the migration, and validating results.

LOC migration uses a statement-based approach. Historical data before the most recent statement date is stored as read-only snapshots. Data after that date is actively replayed by Peach's Loan Replay™ engine. This differs from installment loan migration, which recreates the full loan history from activation.


How LOC migration differs from installment migration

Before diving into the steps, it helps to understand how LOC migration compares to installment loan migration at a high level.

AspectInstallment loanLine of credit
History handlingFull replay from activation dateRead-only history before cutoff; replay after
Object structureSingle loan objectLine (loan) + multiple draws + migration draw
Migration timingAny timeBetween statement date and due date
Balance calculationFully recalculated from scratchSeeded from statement balances
Historical objectsExpected paymentsPast periods, purchases, past transactions (optional — see note below)
Replay behaviorAll events replayed chronologicallyOnly activities after cutoff date replayed

Info: Past periods, historical purchases, and past transactions are not required for migration to succeed. They are read-only objects that display the borrower's history for convenience and reference, but they do not affect balance calculations or loan behavior. It is possible to migrate a loan onto Peach with just the seed balances and overdue information, and the loan will start running as if it was just created with those balances. However, providing historical data is recommended for a complete borrower experience.

The key difference is the migration cutoff date. For LOCs, Peach does not recalculate balances from the beginning of the loan. Instead, you provide balance snapshots as of the most recent statement date, and Peach replays only the activity that occurred after that date. This makes LOC migration faster but requires accurate statement-date balances from your legacy system. You will also need very granular information about your draws — since Peach allows draws with different interest rates, fee structures, and configurations, you must provide balances broken down by draw so Peach knows which balance belongs to which draw.


Key concepts

These terms appear throughout the guide. Understanding them before you start will make each step clearer.

Migration period

The loan period during which migration executes. It begins on the migration cutoff date (your most recent statement date) and ends on the last day of that billing cycle. This is the "live" period — Peach takes control of the loan starting from the migration period's start date.

Example: If your statement generates on the 1st of each month and payment is due on the 22nd:

  • Previous period: Jul 1 – Jul 31 (statement date: Aug 1, due date: Aug 22)
  • Migration period: Aug 1 – Aug 31 (statement date: Sep 1, due date: Sep 22)

Warning: Migration must complete before the previous period's due date that falls within the migration period. In this example, that means migration must finish before Aug 22. If it doesn't, the loan must be canceled and re-migrated. See Canceling and re-migrating loans for the recovery process.

What happens if you miss the migration deadline: If migration is not completed before the previous period's due date, there is currently no in-place recovery path. You must cancel the loan (clearing external IDs), then restart the migration process from the beginning. When you re-create the loan, the data from the original migration period moves into "past periods" (read-only history), and you set up a new migration period based on your next statement date. Note that there is no time limit on the prepMigration phase — you can remain in prepMigration for as long as needed while preparing your data. The deadline only applies to when you call the migrate endpoint.

Migration cutoff date

The most recent statement date relative to the day you execute migration. This date divides all activity into two categories:

  • Before the cutoff date → historical (read-only). Stored as past periods and past transactions.
  • On or after the cutoff date → live. Replayed by Peach during migration.

All balances you provide to Peach must be accurate as of this date. The cutoff date is also the startDate of the migration period.

Warning: Balances should include interest accrued through the last day of the previous period (endDate) but should not include interest for the statement date itself. Peach's system will accrue interest for the statement date during the migration replay. If you include interest for the statement date in your seed balances, the consequence is double interest corresponding to that day.

Migration draw (static draw)

When you create a line of credit with migrationStatus: "prepMigration", Peach automatically creates a special draw called the migration draw. This draw serves as a container for all historical activity that occurred before the migration cutoff date.

The migration draw has specific behaviors:

  • All historical purchases are posted to this draw (with the originalDrawId field referencing the actual legacy draw).
  • It does not accrue interest.
  • It does not participate in the Loan Replay process.
  • After migration completes, it transitions to active status but is effectively disabled — no new activity can be posted to it. The API rejects requests targeting a static draw.

Static draw post-migration behavior: After migration completes, the migration draw receives active status but is functionally inert. It does not participate in fee caps or other calculations that aggregate across draws. All new activity (purchases, payments, fees) must be posted to the actual draws you create in Step 2.

Past periods data

Statement-cycle snapshots from your legacy system, covering the borrower's full history before the migration cutoff date. Each past period includes start/end dates, statement date, due date, and optionally a reference to the uploaded statement document.

Past periods are displayed in the borrower's history but are not replayed — they're read-only records.

Historical vs. live activity

Activity is categorized based on when it occurred relative to the migration cutoff date:

Historical activityLive activity
When it occurredBefore migration cutoff dateOn or after migration cutoff date
Where purchases goMigration draw (with originalDrawId)Actual draw
Transaction endpointPOST .../migration/past-transactionPOST .../transactions (with isExternal: true)
Fee endpointPOST .../fees (with migration.originalDrawId)POST .../fees (normal)
Replayed by Peach?NoYes

Grace period eligibility

For LOC products with grace periods (common for credit cards), the isGracePeriodEligible field in the migration period data controls whether interest accrues during the migration period:

  • true → No interest accrues during the migration period (borrower is in grace).
  • false → Interest begins accruing immediately from the cutoff date.

Grace eligibility is re-evaluated on the day after the due date within the migration period. This means the system checks whether the borrower paid the full statement balance by the due date, and updates grace status accordingly for the next period.

Edge case: If a draw is configured with numPeriodsToRestoreGrace greater than 1 (meaning the borrower must pay in full for multiple consecutive periods to regain grace), this rule is not enforced during the migration period. During migration, if the draw qualifies for grace based on the current period's payment, the system restores grace immediately — regardless of numPeriodsToRestoreGrace. This is by design: since Peach does not store per-draw grace history for past periods, it defaults to the behavior most favorable to the borrower. The numPeriodsToRestoreGrace rule takes full effect starting in the period after the migration period.


Prerequisites

Complete these steps before beginning the migration process.

1. Set up a sandbox environment

Create or confirm access to a Peach sandbox environment. You'll use the sandbox to test the full migration workflow with sample loans before running in production.

Warning: Sandbox has capacity limits. Migrate no more than 5 loans at a time in sandbox. Exceeding this limit may cause significant processing delays or migration failures.

2. Configure your loan type

Create or confirm a Loan Type for your LOC product. The Loan Type defines interest calculation methods, fee structures, payment schedules, and other product-level rules. You'll reference the Loan Type ID (loanTypeId) when creating each loan.

3. Configure fee types

If your LOC product charges dynamic fees (late fees, annual fees, draw fees, foreign transaction fees, modification fees), ensure each fee type is configured in Peach with a Fee Type ID. You'll reference these IDs when creating fees during migration.

Fee type guidance: Configure the fee types you want Peach to charge going forward with the correct settings. Once the loan migrates, Peach will continue charging these fees according to the loan type configuration. It is also possible to post live fees on the loan during the replay period, so configure fee types that match your production fee structure as closely as possible.

4. Prepare your legacy data

Extract the following data from your legacy system. You'll need it throughout the migration process:

  • Borrower data: Name, date of birth, SSN/identity, contact information (addresses, emails, phone numbers), consent records
  • Loan terms: Original activation date, credit limit, interest rates (including promo rates if applicable), APR, payment frequency, due date schedule, grace period terms, minimum payment calculation rules
  • Statement history: Start/end dates, statement dates, due dates, and statement balances for each historical billing cycle
  • Draw data: Active draws with their individual credit limits, interest rates, and balances
  • Purchase history: Every purchase with date, amount, merchant details, draw association, and current status
  • Transaction history: Every payment, service credit, and down payment with date, amount, payment instrument, status, and draw allocation
  • Fee history: Every fee charged with type, amount, date, and associated draw/purchase
  • Balance snapshot as of the migration cutoff date: Current balances broken down by due/non-due/overdue for both the line level and each draw, including principal, interest, and each fee type
  • Delinquency data: Days past due, overdue amounts, date from which account is overdue (if applicable)
  • Payment instruments: Active and historical bank accounts, cards, and other payment methods

5. Plan your migration timing

Migration must be executed within a specific window:

  1. The earliest you can migrate is immediately after the migration cutoff date (your most recent statement date).
  2. The latest you should migrate is before the previous period's due date that falls within the migration period.

There is no time limit on the prepMigration phase — you can remain in prepMigration for as long as needed while you prepare and validate your data. The timing window above applies only to when you call the POST /migrate endpoint.

Example timeline:

  • The previous period runs from Jul 1 through Jul 31
  • The statement date is Aug 1
  • The due date is Aug 22
  • The migration period runs from Aug 1 through Aug 31

In this scenario, the earliest you can migrate is Aug 1 (the cutoff date, which is the most recent statement date). The latest you should migrate is before Aug 22 (the previous period's due date, which falls within the migration period).


Step 1: Create the borrower and payment methods

Establish the borrower's identity and payment capabilities in Peach.

Create borrower

Create a borrower record with personal information. You'll use the returned personId in all subsequent API calls.

POST /api/people
Content-Type: application/json

{
  "status": "active",
  "externalId": "your-borrower-id-123",
  "name": {
    "firstName": "Jane",
    "middleName": "A",
    "lastName": "Smith"
  },
  "dateOfBirth": "1985-03-15",
  "identity": {
    "identityType": "SSN",
    "value": "123456789"
  }
}

Response (abbreviated):

{
  "data": {
    "id": "BO-1234-ABCD",
    "externalId": "your-borrower-id-123",
    "status": "active"
  }
}

Create contact records

Create separate contact records for each piece of contact information. This separation enables contact-level management and history tracking.

POST /api/people/{personId}/contacts
Content-Type: application/json

{
  "contactType": "address",
  "label": "home",
  "affiliation": "self",
  "status": "primary",
  "address": {
    "addressLine1": "742 Evergreen Terrace",
    "city": "San Francisco",
    "state": "CA",
    "postalCode": "94105",
    "country": "US"
  }
}

Repeat for each email address, phone number, and additional address. Each contact record includes a contactType (address, email, phone), a label (home, work, mobile), and an affiliation (self, spouse, etc.).

Create payment instruments

Create payment instruments for both active and historical payment methods. Active methods will be used for future payments; historical methods are needed for accurate transaction records.

Active payment instrument (full account details):

POST /api/people/{personId}/payment-instruments
Content-Type: application/json

{
  "status": "active",
  "verified": true,
  "nickname": "Primary Checking",
  "instrumentType": "bankAccount",
  "accountNumber": "9876543210",
  "routingNumber": "021000021",
  "accountType": "checking",
  "accountHolderType": "personal",
  "accountHolderName": "Jane A Smith"
}

Historical payment instrument (last four digits only):

For payment methods no longer in use, set isExternal: true. This requires only the last four digits of the account number.

POST /api/people/{personId}/payment-instruments
Content-Type: application/json

{
  "status": "active",
  "verified": true,
  "isExternal": true,
  "nickname": "Old Checking (closed)",
  "instrumentType": "bankAccount",
  "accountNumberLastFour": "5678",
  "accountType": "checking",
  "accountHolderType": "personal",
  "accountHolderName": "Jane A Smith"
}

You can also provide inline payment instrument details directly on migration transactions using paymentInstrumentDetails, which accepts the following types: bankAccount, creditCard, debitCard, check, payroll, moneyOrder, wire, and custom.

Info: Only payment instruments with status: "active" can be used for future payments. Historical instruments created with isExternal: true are for record-keeping only.


Step 2: Create and configure the line of credit

Create the loan object, retrieve the auto-generated migration draw, create any additional active draws, and upload loan documents.

Create the loan

Create the line of credit with status: "pending" and migration.migrationStatus: "prepMigration". The atOrigination object defines the loan's terms; the migration object tells Peach this loan is being migrated.

POST /api/people/{personId}/loans
Content-Type: application/json

{
  "externalId": "your-loc-id-789",
  "loanTypeId": "LT-LOC-ABCD",
  "type": "lineOfCredit",
  "servicedBy": "creditor",
  "status": "pending",
  "muteLoanNotices": true,
  "atOrigination": {
    "paymentFrequency": "monthly",
    "specificDays": [22],
    "interestRates": [
      { "days": null, "rate": 0.1999 }
    ],
    "promoRates": [],
    "aprEffective": 0.2149,
    "aprNominal": 0.1999,
    "creditLimitAmount": 10000.00,
    "gracePeriod": {
      "enabled": true,
      "numDays": 25,
      "numPeriodsToRestoreGrace": 1
    },
    "minPaymentCalculation": {
      "percentageOfPrincipal": 0.02,
      "minAmount": 25.00,
      "includeFeesInCalculation": true,
      "includeInterestInCalculation": true
    },
    "personAddressId": "CT-ADDR-ABCD",
    "skipCreditReporting": false
  },
  "migration": {
    "migrationStatus": "prepMigration",
    "activatedDate": "2023-06-15",
    "activatedTimeOfDay": {
      "hour": 10,
      "minute": 0,
      "second": 0
    }
  }
}

Response (abbreviated):

{
  "data": {
    "id": "LN-1234-ABCD",
    "type": "lineOfCredit",
    "status": "pending",
    "migrationStatus": "prepMigration"
  }
}

Info: Setting muteLoanNotices: true is not strictly required — Peach will not send borrower notifications during the migration process. However, setting it to true provides an extra safeguard and gives you explicit control over when communications resume after migration. See Borrower communications during migration for details.

Key fields in atOrigination:

FieldDescription
paymentFrequency / specificDaysCurrent billing frequency and due date day(s) of month. specificDays must align with the dueDate in the migration period data — e.g., if dueDate is the 22nd, set specificDays: [22].
interestRatesInterest rate schedule. Set from the migration cutoff date. Use days: null for an indefinite rate.
promoRatesPromotional rate schedule, if applicable
creditLimitAmountCredit limit at origination
gracePeriodGrace period configuration. numDays is the number of days after the statement date during which no interest accrues if the full balance is paid.
minPaymentCalculationRules for calculating the minimum payment each cycle
skipCreditReportingSet to true if you do not report this loan to credit bureaus

Key fields in migration:

FieldDescription
migrationStatusMust be "prepMigration" to begin the process
activatedDateThe original date the line of credit was opened in your legacy system
activatedTimeOfDayTime of activation (hour, minute, second in product timezone)

Retrieve the migration draw

When you create a loan with migrationStatus: "prepMigration", Peach automatically creates the migration draw. Retrieve it to get its ID — you'll need this when posting historical purchases and fees.

GET /api/people/{personId}/loans/{loanId}/draws

Response (abbreviated):

{
  "data": [
    {
      "id": "DR-MIGR-ABCD",
      "nickname": "Migration Draw",
      "drawType": "static",
      "status": "pending"
    }
  ]
}

The migration draw has drawType: "static". Save this ID for Step 4.

Create active draws

Create any draws that should exist after the migration cutoff date. These are the "real" draws that will be active post-migration — they accrue interest, accept new purchases, and participate in billing.

Only create draws that should be live after the cutoff date. Historical draws that have been closed or fully paid off don't need to be created as separate draws — their activity goes to the migration draw.

POST /api/people/{personId}/loans/{loanId}/draws
Content-Type: application/json

{
  "externalId": "your-draw-id-001",
  "nickname": "Primary Draw",
  "status": "pending",
  "atOrigination": {
    "interestRates": [
      { "days": null, "rate": 0.1999 }
    ],
    "creditLimitAmount": 8000.00,
    "minPaymentCalculation": {
      "percentageOfPrincipal": 0.02,
      "minAmount": 25.00
    },
    "gracePeriod": {
      "enabled": true,
      "numDays": 25,
      "numPeriodsToRestoreGrace": 1
    }
  }
}

Response (abbreviated):

{
  "data": {
    "id": "DR-0001-ABCD",
    "externalId": "your-draw-id-001",
    "status": "pending"
  }
}

Repeat for each active draw. Save each draw's id — you'll need them for migration period data and live activities.

Upload loan documents

Upload loan agreements, historical statements, and any other relevant documents. Create a document descriptor first, then upload the file content.

POST /api/people/{personId}/documents
Content-Type: application/json

{
  "type": "loanAgreement",
  "description": "Original LOC Agreement",
  "status": "accepted",
  "loanId": "LN-1234-ABCD",
  "sensitiveData": false
}
POST /api/people/{personId}/documents/{documentDescriptorId}/content
Content-Type: multipart/form-data

file=@/path/to/agreement.pdf

If you're providing historical statements for past periods (Step 3), save each document descriptor ID — you'll reference it when creating past period data.


Step 3: Populate historical data

Provide Peach with the borrower's historical billing cycles and set up the migration period with current balances. This step has three parts: past periods, migration period LOC data, and migration period draw data.

Create past periods data

Past periods are billing-cycle snapshots from your legacy system covering the borrower's history before the migration cutoff date. They appear in the borrower's statement history but are not replayed.

Period date rules:

  • Periods must be contiguous — no gaps between them. The startDate of each period must equal the day after the previous period's endDate.
  • The statementDate for each period must equal the day after the period's endDate.
  • The dueDate must fall within the following period's date range.
  • No two periods can have overlapping start-end date ranges.
POST /api/people/{personId}/loans/{loanId}/migration/past-periods
Content-Type: application/json

[
  {
    "startDate": "2024-04-01",
    "endDate": "2024-04-30",
    "statementDate": "2024-05-01",
    "dueDate": "2024-05-22",
    "statement": {
      "documentDescriptorId": "DD-APR-ABCD",
      "creditBalanceAmount": 0.00,
      "minimumAmountDue": 125.00,
      "newBalanceAmount": 3450.75
    },
    "gracePeriod": {
      "fullBalanceAmount": 3450.75,
      "fullBalanceMinusOverdueAmount": 3450.75,
      "isGracePeriodEligible": true
    }
  },
  {
    "startDate": "2024-05-01",
    "endDate": "2024-05-31",
    "statementDate": "2024-06-01",
    "dueDate": "2024-06-22",
    "statement": {
      "documentDescriptorId": "DD-MAY-ABCD",
      "creditBalanceAmount": 0.00,
      "minimumAmountDue": 118.50,
      "newBalanceAmount": 3200.00
    },
    "gracePeriod": {
      "fullBalanceAmount": 3200.00,
      "fullBalanceMinusOverdueAmount": 3200.00,
      "isGracePeriodEligible": true
    }
  }
]

Include all historical periods from the loan's activation through the period immediately before the migration period. The statement.documentDescriptorId is optional — include it if you uploaded the corresponding statement document.

Grace period fields in past periods:

FieldDescription
fullBalanceAmountTotal balance eligible for grace period evaluation
fullBalanceMinusOverdueAmountFull balance minus any overdue amount
totalFulfilledOnDueDateAmountAmount paid by the due date. Set this on the period one day after the due date.
isGracePeriodEligibleWhether the borrower maintained grace eligibility through this period

Create migration period LOC data

The migration period LOC data represents the line-level state as of the migration cutoff date. This is where you provide the balance snapshot and obligation information that Peach uses as its starting point.

POST /api/people/{personId}/loans/{loanId}/migration/period
Content-Type: application/json

{
  "startDate": "2024-08-01",
  "endDate": "2024-08-31",
  "statementDate": "2024-09-01",
  "dueDate": "2024-09-22",
  "balances": {
    "nonDueBalances": {
      "nonDueOriginationFeesAmount": 0.00,
      "nonDueLateFeesAmount": 0.00
    },
    "dueBalances": {
      "dueOriginationFeesAmount": 0.00,
      "dueLateFeesAmount": 0.00
    },
    "overdueBalances": {
      "overdueOriginationFeesAmount": 0.00,
      "overdueLateFeesAmount": 0.00
    },
    "creditLimitAmount": 10000.00,
    "reimbursementAmount": 0.00
  },
  "obligation": {
    "obligationAmount": 125.00,
    "migratedDaysOverdue": 0,
    "migratedOverdueFromDate": null
  },
  "gracePeriod": {
    "isGracePeriodEligible": true,
    "fullBalanceAmount": 2850.00,
    "fullBalanceMinusOverdueAmount": 2850.00
  }
}

Balance fields at the line level include only origination fees and late fees broken down by non-due, due, and overdue. The line level can only carry fee balances — principal and interest balances are tracked at the draw level, not the line level. Do not post principal or interest balance totals on the line.

FieldDescription
creditLimitAmountCurrent overall credit limit for the line
reimbursementAmountAmount the lender owes to the borrower (credit balances). Set to 0.00 if not applicable.
obligation.obligationAmountRemaining amount the borrower must pay by the upcoming due date
obligation.migratedDaysOverdueNumber of days the account was overdue as of the cutoff date. Set to 0 if current.
obligation.migratedOverdueFromDateDate from which the account has been overdue. Set to null if the account is current, or if the overdue date is the same as the migration cutoff date. For example, if you are migrating on 2/1, the migration cutoff is 2/1, and the loan is overdue 7 days, you can set migratedOverdueFromDate to null and migratedDaysOverdue to 7.

Info: All balance amounts must be ≥ 0.00 with a precision of 0.01 (two decimal places).

Create migration period draw data

For each active draw (not the migration draw), create migration period data with draw-level balances and obligations.

POST /api/people/{personId}/loans/{loanId}/draws/{drawId}/migration/period
Content-Type: application/json

{
  "balances": {
    "nonDueBalances": {
      "nonDuePrincipalAmount": 2200.00,
      "nonDueInterestAmount": 0.00,
      "nonDueDrawFeesAmount": 0.00,
      "nonDueLateFeesAmount": 0.00,
      "nonDueModificationFeesAmount": 0.00
    },
    "dueBalances": {
      "duePrincipalAmount": 50.00,
      "dueInterestAmount": 37.50,
      "dueDrawFeesAmount": 0.00,
      "dueLateFeesAmount": 0.00,
      "dueModificationFeesAmount": 0.00
    },
    "overdueBalances": {
      "overduePrincipalAmount": 0.00,
      "overdueInterestAmount": 0.00,
      "overdueDrawFeesAmount": 0.00,
      "overdueLateFeesAmount": 0.00,
      "overdueModificationFeesAmount": 0.00
    },
    "creditLimitAmount": 8000.00
  },
  "obligation": {
    "obligationAmount": 87.50,
    "migratedDaysOverdue": 0
  },
  "gracePeriod": {
    "isGracePeriodEligible": true,
    "fullBalanceAmount": 2287.50,
    "fullBalanceMinusOverdueAmount": 2287.50
  }
}

Draw-level balances provide a more granular breakdown than line-level balances, including principal, interest, draw fees, late fees, and modification fees for each draw.

Late fees vs. modification fees: Peach maintains separate ledger buckets for late fees and modification fees. You must specify which fee type each balance belongs to — the system needs to know the correct ledger bucket for each amount. Ensure your lateFeesAmount and modificationFeesAmount fields accurately reflect the fee type from your legacy system.

Repeat this call for every active draw. Note that line-level and draw-level balances are independent — the line level only carries fee balances (origination fees, late fees), while draws carry principal, interest, and draw-specific fees. Do not attempt to reconcile draw-level totals against line-level totals, as there is validation that prevents the line from having principal or interest balances. Similarly, the sum of draw-level creditLimitAmount values should not exceed the line-level credit limit.

Change loan status to Originated

After populating all historical data, change the loan status from pending to originated. This locks the atOrigination data.

PUT /api/people/{personId}/loans/{loanId}
Content-Type: application/json

{
  "status": "originated"
}

Warning: After changing status to originated, you cannot modify the atOrigination object. Make sure all loan terms, interest rates, and configuration are correct before this step.


Step 4: Create activities

Create all purchases, transactions (payments), and fees — both historical and live. The distinction between historical and live determines which endpoint you use and which draw receives the activity.

Historical purchases (before the migration cutoff date)

Post historical purchases to the migration draw, not to the actual draws. Use the migration.originalDrawId field to record which legacy draw the purchase originally belonged to. This enables proper balance adjustments if the purchase is later disputed.

POST /api/people/{personId}/loans/{loanId}/draws/{migrationDrawId}/purchases
Content-Type: application/json

{
  "externalId": "your-purchase-id-001",
  "type": "regular",
  "status": "settled",
  "amount": 249.99,
  "purchaseDate": "2024-07-10",
  "purchaseDetails": {
    "description": "Electronics Store",
    "pointOfSaleType": "inStore",
    "merchantName": "Best Buy #1234",
    "merchantNumber": "MRC-9876"
  },
  "migration": {
    "originalDrawId": "your-legacy-draw-id-001"
  }
}

Info: Purchases in authorized status are not allowed in past periods (before the migration cutoff date). All historical purchases must have a terminal status such as settled, canceled, or returned.

Live purchases (on or after the migration cutoff date)

Post live purchases to the actual draw they belong to. Do not include the migration object.

POST /api/people/{personId}/loans/{loanId}/draws/{drawId}/purchases
Content-Type: application/json

{
  "externalId": "your-purchase-id-042",
  "type": "regular",
  "status": "settled",
  "amount": 75.50,
  "purchaseDate": "2024-08-05",
  "purchaseDetails": {
    "description": "Grocery Store",
    "pointOfSaleType": "inStore",
    "merchantName": "Whole Foods #567"
  }
}

Historical transactions (before the migration cutoff date)

Use the migration-specific past-transaction endpoint for payments that occurred before the cutoff date. Include drawSplitDetails to record how the payment was allocated across draws — this is essential for handling reversals or status changes after migration.

POST /api/people/{personId}/loans/{loanId}/migration/past-transaction
Content-Type: application/json

{
  "externalId": "your-payment-id-001",
  "amount": 200.00,
  "type": "oneTimePayment",
  "status": "succeeded",
  "effectiveDate": "2024-07-22",
  "effectiveTimeOfDay": {
    "hour": 14,
    "minute": 30,
    "second": 0
  },
  "paymentInstrumentId": "PI-1234-ABCD",
  "migration": {
    "drawSplitDetails": [
      {
        "originalDrawId": "your-legacy-draw-id-001",
        "drawAllocatedAmount": 200.00
      }
    ]
  }
}

Transaction types supported:

  • oneTimePayment — Standard payment
  • serviceCredit — Credit applied to the account (note: you cannot pass status for service credits — the system automatically sets it to succeeded)
  • downPayment — Down payment

Status options (for oneTimePayment and downPayment): initiated, pending, succeeded, failed, canceled, inDispute, chargeback

Warning: drawSplitDetails are critical for post-migration modifications. If you need to change a historical transaction's status after migration (e.g., from succeeded to failed), Peach uses the drawSplitDetails to determine which draw balances to adjust. If you omit drawSplitDetails, post-migration status changes on that transaction will fail.

Important constraints:

  • Only reference active draw IDs in drawSplitDetails — do not use the migration draw ID.
  • drawSplitDetails are only returned by the GET .../migration/past-transaction endpoint, not the standard GET .../transactions endpoint.

Inline payment instrument details: If you don't want to create a separate payment instrument record for a historical transaction, you can provide details inline:

{
  "paymentInstrumentDetails": {
    "type": "bankAccount",
    "accountLastFour": "5678",
    "customDisplayName": "Legacy Checking"
  }
}

Live transactions (on or after the migration cutoff date)

Use the standard transaction endpoint with isExternal: true for payments processed by your legacy system after the cutoff date.

POST /api/people/{personId}/loans/{loanId}/transactions
Content-Type: application/json

{
  "externalId": "your-payment-id-042",
  "paymentInstrumentId": "PI-1234-ABCD",
  "amount": 150.00,
  "type": "oneTime",
  "status": "succeeded",
  "isExternal": true,
  "effectiveDate": "2024-08-15",
  "effectiveTimeOfDay": {
    "hour": 10,
    "minute": 0,
    "second": 0
  }
}

Info:

  • The type field differs between endpoints: the past-transaction endpoint uses oneTimePayment, serviceCredit, and downPayment, while the standard transaction endpoint uses oneTime, serviceCredit, etc. Use the correct type value for the endpoint you're calling.
  • The effectiveTimeOfDay must be after 2:00 AM in the product timezone. Transactions with earlier times will fail validation.

Historical fees (before the migration cutoff date)

Create fees using the standard fee endpoint, but include migration.originalDrawId to associate the fee with the correct legacy draw. For purchase-related fees (e.g., foreign transaction fees), also include migration.originalPurchaseId.

POST /api/people/{personId}/loans/{loanId}/fees
Content-Type: application/json

{
  "feeTypeId": "FT-LATE-ABCD",
  "amount": 29.00,
  "chargeDate": "2024-07-25",
  "chargeTimeOfDay": {
    "hour": 10,
    "minute": 0,
    "second": 0
  },
  "migration": {
    "originalDrawId": "your-legacy-draw-id-001"
  }
}

Fee types with special migration fields:

Fee typeAdditional migration fieldDescription
Late fees, annual feesmigration.originalDrawIdAssociates with a legacy draw. Omit for line-level fees.
NSF feesmigration.originalTransactionIdAssociates with the failed transaction
Foreign transaction feesmigration.originalPurchaseIdAssociates with the purchase that triggered the fee

Live fees (on or after the migration cutoff date)

Create live fees normally, without the migration object.

POST /api/people/{personId}/loans/{loanId}/fees
Content-Type: application/json

{
  "feeTypeId": "FT-LATE-ABCD",
  "amount": 29.00,
  "chargeDate": "2024-08-25",
  "chargeTimeOfDay": {
    "hour": 10,
    "minute": 0,
    "second": 0
  }
}

Step 5: Execute migration

With all data in place, trigger the migration process. Peach validates the prepared data, replays live activities, and activates the loan.

Call the migrate endpoint

POST /api/people/{personId}/loans/{loanId}/migrate
Content-Type: application/json

{
  "sync": false
}
ParameterDescription
sync: trueSynchronous — the API call blocks until migration completes or fails. Timeout: 60 seconds. If migration takes longer, the call returns 408 Request Timeout but migration continues in the background. Use for single-loan testing only.
sync: falseAsynchronous — the API call returns immediately and migration runs in the background. Recommended for production and bulk migrations.

Monitor migration status

For asynchronous migrations, poll the loan endpoint to check progress:

GET /api/people/{personId}/loans/{loanId}

Watch the migrationStatus field:

StatusMeaning
prepMigrationData preparation phase (pre-migrate call)
migratingMigration in progress — Peach is replaying live events
completedMigration completed successfully
failedMigration failed — data has been rolled back

What happens during migration

When you call the migrate endpoint, Peach performs the following sequence:

  1. Validates all migration data (periods, balances, transactions, draws).
  2. Locks the loan to prevent concurrent modifications.
  3. Originates the loan if it's in pending status.
  4. Sets line-level grace period information from the migration period data.
  5. Activates the LOC and updates draw information for the migration period.
  6. Generates daily ledger management events.
  7. Replays all live activities (purchases, transactions, fees) in chronological order.
  8. Sets migrationStatus to completed.
  9. Fires a loan.migration.succeeded webhook event.
  10. Fires webhooks for all events created for live activities.

If migration succeeds

  • The loan status progresses from pendingoriginatedactive.
  • migrationStatus changes to completed.
  • A loan.migration.succeeded event is created and a webhook fires.
  • All live activities are replayed and reflected in the loan's balance.
  • The loan begins accruing interest and generating billing events according to its configuration.

If migration fails

  • The system rolls back the ledger — no partial state is left.
  • The loan status reverts to pending.
  • migrationStatus changes to failed.
  • A loan.migration.failed event is created and a webhook fires.

To retry after a failure:

  1. Review the error details in the migration event.
  2. Correct the issue (fix data, adjust balances, etc.).
  3. Reset migrationStatus to prepMigration:
PUT /api/people/{personId}/loans/{loanId}
Content-Type: application/json

{
  "migration": {
    "migrationStatus": "prepMigration"
  }
}
  1. Make any necessary data corrections.
  2. Call the migrate endpoint again.

Info: If migrationStatus shows failed but you don't see specific errors, contact Peach Support for investigation.


Step 6: Post-migration

After a successful migration, validate the results, adjust any discrepancies, and configure ongoing features.

Validate balances

Compare the following values between Peach and your legacy system:

  1. Total outstanding balance (principal + interest + fees)
  2. Due amounts (minimum payment due)
  3. Overdue amounts and days past due
  4. Credit limit and available credit
  5. Individual draw balances
  6. Next payment due date and amount
  7. Interest rate(s) and any active promo rates
  8. Payment history accuracy
  9. Migrated payment instruments

Use the following endpoints for validation:

GET /api/people/{personId}/loans/{loanId}/balance
GET /api/people/{personId}/loans/{loanId}/draws/{drawId}/balance
GET /api/people/{personId}/loans/{loanId}/expected-payments

Adjust balances

If there's a discrepancy between Peach and your legacy system:

If Peach balance is higher → apply a service credit:

POST /api/people/{personId}/loans/{loanId}/transactions
Content-Type: application/json

{
  "amount": 5.43,
  "type": "serviceCredit",
  "serviceCreditsDetails": {
    "reason": "Migration balance adjustment",
    "sponsor": "loanServicer"
  },
  "effectiveDate": "2024-08-01",
  "effectiveTimeOfDay": {
    "hour": 12,
    "minute": 0,
    "second": 0
  }
}

If Peach balance is lower → apply an adjustment fee or leave as-is (in the borrower's favor):

POST /api/people/{personId}/loans/{loanId}/fees
Content-Type: application/json

{
  "feeTypeId": "FT-ADJ-ABCD",
  "amount": 3.21,
  "chargeDate": "2024-08-01",
  "chargeTimeOfDay": {
    "hour": 12,
    "minute": 0,
    "second": 0
  }
}

Info: The adjustment fee type (adHoc with a custom display name like "Balance Adjustment") must be configured by Peach. Contact your implementation team if you don't have one set up.

Configure autopay

If the borrower was enrolled in autopay on the legacy system, set it up in Peach after migration completes.

POST /api/people/{personId}/loans/{loanId}/autopay
Content-Type: application/json

{
  "type": "lineOfCredit",
  "paymentInstrumentId": "PI-1234-ABCD",
  "autopayAmountLogic": "statement_minimum_amount",
  "paymentScheduleDayOfMonth": 22,
  "paymentFrequency": "monthly",
  "consentDocumentId": "DD-CONSENT-ABCD"
}

Autopay timing: Configure autopay after the migrate call completes, not before. Peach does not process autopay payments during the migration replay period — if an autopay payment would have been due during the migration window, the system silently skips it. Be aware that it is possible to configure autopay schedules that don't align with your LOC billing frequency (e.g., twiceMonthly autopay on a monthly LOC), and there is currently no validation to prevent this, so ensure your autopay configuration matches your billing cycle.

Re-enable borrower communications {#re-enable-borrower-communications}

Once you're confident the migration is successful and balances are correct, re-enable borrower notifications:

PUT /api/people/{personId}/loans/{loanId}
Content-Type: application/json

{
  "muteLoanNotices": false
}

Warning: Don't re-enable communications until you've completed balance validation and any necessary adjustments. Premature re-enablement can trigger confusing notifications to borrowers (e.g., payment reminders with incorrect amounts).

Set up credit reporting

If you report to credit bureaus, configure credit reporting during loan creation to maintain history continuity. You have two options.

For a full reference on Peach's credit reporting system, see the Metro 2® Credit Reporting Logic Guide. The sections most relevant to migration are:

Option 1: Report with payment history (provide paymentHistoryProfile)

Include the borrower's Payment History Profile from the legacy system. This preserves up to 24 months of payment history on credit reports, ensuring no gap in the borrower's credit record.

Pass the following fields in the migration.creditReporting object when creating the loan:

{
  "migration": {
    "migrationStatus": "prepMigration",
    "activatedDate": "2023-06-15",
    "activatedTimeOfDay": { "hour": 10, "minute": 0, "second": 0 },
    "creditReporting": {
      "paymentHistoryProfile": "000000000000000000000000",
      "dateOfFirstDelinquency": null,
      "k2Segment": {
        "purchasedFromName": "LEGACY LENDER INC"
      },
      "l1Segment": {
        "changeIndicator": "3",
        "oldConsumerAccountNumber": "LEGACY-ACCT-12345",
        "oldIdentificationNumber": "LEGACY-ID-6789"
      }
    }
  }
}
FieldDescription
paymentHistoryProfile24-character string representing the last 24 months of payment history per the Metro 2 spec. Each character is a payment rating code. Most recent month first. See character definitions below.
dateOfFirstDelinquencyFCRA Date of First Delinquency (DOFD), if applicable. This value is provided as part of the migration process and is used directly by Peach the very first time the LOC is reported to credit bureaus. It does not interact with migratedDaysOverdue — they are independent fields. Set to the date the account first became 30+ days past due on the legacy system. null if the account has never been delinquent. See Field Specifications — FCRA Date of First Delinquency for how Peach uses this value post-migration.
k2Segment.purchasedFromNameName of the entity the account was purchased or transferred from. Reported in the Metro 2 K2 Segment with a Purchased From indicator (1). Maximum 30 characters, uppercase. See Field Specifications — K2 Segment.
l1Segment.changeIndicatorIndicates what changed during migration. "1" = Consumer Account Number changed only. "2" = Identification Number changed only. "3" = both changed. See Field Specifications — L1 Segment.
l1Segment.oldConsumerAccountNumberThe Consumer Account Number from the legacy system. Peach reports this old number in the Base Segment and the new Peach account number in the L1 Segment, so bureaus can link the records.
l1Segment.oldIdentificationNumberThe Identification Number (subscriber code) from the legacy system. Same reporting logic as the account number — old value in the Base Segment, new value in L1.

Info: The K2 and L1 segment fields are independent of the Payment History Profile. You can provide a paymentHistoryProfile without K2/L1 segments, or vice versa, depending on your reporting needs.

Payment History Profile character definitions:

The paymentHistoryProfile string uses the standard Metro 2 character set. Use the codes from your legacy system's reporting data:

CharacterMeaning
0Current (0–29 days past due)
130–59 days past due
260–89 days past due
390–119 days past due
4120–149 days past due
5150–179 days past due
6180+ days past due
BNo payment history available for this month
DNo payment due (e.g., forbearance, bankruptcy)
EZero balance / current (open-ended accounts)
LCharge-off

For a complete reference on how Peach calculates the Payment History Profile post-migration (including how the migrated profile merges with live data), see Payment Rating and History.

Warning: The paymentHistoryProfile string must be exactly 24 characters. If the account has fewer than 24 months of history, pad the remaining positions (oldest months) with B to indicate no prior history. For example, an account with 12 months of current history: 000000000000BBBBBBBBBBBB.

Option 2: Report as new account (omit paymentHistoryProfile)

If you don't have the borrower's Payment History Profile or prefer to start fresh, simply omit the paymentHistoryProfile field from the creditReporting object:

{
  "migration": {
    "creditReporting": {
      "dateOfFirstDelinquency": null
    }
  }
}

With this option, Peach reports the account without historical payment data. For months prior to the migration cutoff date, the Payment History Profile will show B (no payment history available). History begins building from the migration date forward based on live loan performance.

Charge-off reason during migration: For accounts that were charged off on the legacy system, set the chargedOffReason value to match your legacy system's reason. Accepted values are term, fraudulent, bankruptcy, and legal. Matching the reason ensures consistency between your legacy reporting and Peach's ongoing reporting.

System behavior after migration succeeds

Once migrationStatus transitions to completed, several automated processes begin running on the migrated loan. Understanding this sequence helps you validate results and avoid surprises during the first billing cycle.

When daily loan maintenance (DLM) starts

Immediately after migration succeeds, Peach generates DLM events for the loan starting from the interest_start_date through today. DLM then runs daily (triggered by Google Cloud Scheduler at the start of US Pacific Time) for all subsequent days.

For migrated loans, DLM behavior depends on the loan's post-migration status:

Post-migration statusDLM runs?Interest accrues?
activeYesYes
acceleratedYesNo
chargedOffNoNo

Tip: DLM is also responsible for balance movement between non-due, due, and overdue buckets, firing overdue/current events (used for webhooks), and charging late fees or service fees via a separate worker. For a deeper explanation of how DLM works for LOCs, see Appendix A: LOC Billing Cycle Mechanics.

First post-migration statement

The statement for the migration previous period (the billing cycle immediately before the cutoff date) is generated during the migrate call itself, not at the next statement date. Peach populates the previous period's statement with draw balances from the ledger update events you provided in the migration period data. This statement is used for autopay generation.

The first new statement generates at the next statementDate boundary after migration. For example, if your migration period runs Aug 1–Aug 31 with a statement date of Sep 1, the first new statement generates on Sep 1.

Snapshot backfill

After migration completes, Peach queues a snapshot backfill that regenerates balance snapshots for every day between the migration cutoff date and the current date. This ensures that balance history is continuous for reporting and loan tape purposes.

Info: The snapshot backfill process is being improved for greater reliability. Note that you should not see interest accrual on the migration cutoff date + 1 day — the backfill preserves historical consistency by not introducing interest charges for the cutoff boundary day.

How ongoing activity works

After migration, the way you create activity changes:

ActivityDuring migrationAfter migration
PurchasesPosted to migration draw with originalDrawIdPosted to actual draws (POST /draws/{drawId}/purchases)
TransactionsPOST .../transactions with isExternal: truePOST .../transactions (standard — no isExternal flag)
FeesPOST .../fees with migration.originalDrawIdPOST .../fees (standard)
Payment instrumentsisExternal: true for historical instrumentsStandard instruments

Warning: After migration, the migration draw (drawType=Static) is disabled. Do not attempt to post new activity to it — the API will reject requests targeting a static draw with the error "This endpoint is invalid for a static draw."

What happens at the next billing cycle boundary

The first post-migration billing cycle follows the standard LOC balance movement timeline:

  1. On statement date: Non-due balances move to due. The amount moved equals the remainingAmount from the obligation for that period. Interest is truncated at this step — any fractional cents are moved to a forgone_interest_rounding bucket. See Appendix A: LOC Billing Cycle Mechanics for details.
  2. Between statement date and due date: Balances sit in "due" buckets for 2–3 weeks. Borrowers make payments during this window. For LOC products with grace periods, payments applied during this window may have their effectiveDate set to the statement date for interest calculation purposes. See Appendix B: Grace Period for Lines of Credit.
  3. On due date + 1: Any remaining due balance moves to overdue. The system fires loan_overdue events, checks grace period eligibility, and evaluates whether the loan qualifies for acceleration or charge-off based on the loan type's overdue day threshold.

What you can and can't change after migration {#what-you-can-and-cant-change-after-migration}

After migration succeeds, you may need to correct data or handle borrower disputes. Peach supports a specific set of post-migration modifications, with constraints that reflect the migration draw's read-only nature and the system's data integrity rules.

Supported modifications

ActionHowNotes
Dispute a historical purchasePOST .../draws/{drawId}/purchases/{purchaseId}/disputesCreates a type: "refund" purchase with an effective date after the cutoff. Lowers draw balance using standard refund functionality.
Waive or cancel a historical feePUT .../fees/{feeId}/cancelChanges fee status to canceled and depletes the fee balance. If fee balance is already zero, depletes draw principal instead.
Change a past transaction's statusPUT .../migration/past-transaction/{txnId}Allowed transitions: pendingsucceeded, and pending/succeededfailed. For transitions to failed, the system uses drawSplitDetails from the original transaction to increase draw balances.
Reverse a historical transactionPOST .../transactions/{txnId}/reverseCreates a reversal transaction. For each entry in drawSplitDetails, increases the corresponding draw's non-due principal balance.
Correct and re-create with same external IDClear the external ID on the original object, then create a new oneFor purchases: update with externalId: null. For transactions: cancel with clearExternalId: true.
Apply balance adjustmentsService credits (POST .../transactions) or adjustment fees (POST .../fees)Use for post-validation balance discrepancies. See Adjust balances.

Constraints on modifications

These constraints exist because of how the migration draw and historical data are stored:

  • Migration draw is disabled after migration. You cannot post new purchases, transactions, or fees to the static draw. All new activity must target actual draws.

  • drawSplitDetails required for past-transaction status changes to failed. When marking a past transaction as failed, the system needs to know which draws to increase balances for. If drawSplitDetails is null (which can happen for transactions that pre-date the migration cutoff), the status change to failed will be rejected.

  • drawSplitDetails only available via migration endpoint. Past-transaction drawSplitDetails are returned by GET .../migration/past-transaction, not by the standard GET .../transactions endpoint. If you need draw allocation details for a historical transaction, always use the migration-specific endpoint.

  • Past periods are read-only. Statement-cycle snapshots created via POST .../migration/past-periods cannot be modified after migration succeeds. If you need to correct a past period, you would need to cancel and re-migrate the loan.

  • Cannot reverse acceleration or charge-off for loans migrated with a non-null postMigrationLoanStatus. If a loan was created with postMigrationLoanStatus: "accelerated" or postMigrationLoanStatus: "chargedOff" (meaning it was in that state before the cutoff date), the system blocks reverse-acceleration and reverse-charge-off operations. Loans that are accelerated or charged off after migration (through normal DLM processing) can be reversed normally.

  • Cannot add new past periods after migration. The past-periods endpoints are only available while the loan is in prepMigration status.

What the system won't let you do

ActionWhy
Post new activity to the migration drawStatic draws are disabled after migration
Modify past period dataPast periods are read-only snapshots
Change migrationStatus back to prepMigrationMigration status can only move forward in the lifecycle
Reverse accel/charge-off for loans created with postMigrationLoanStatusThese status changes preceded the migration cutoff; the system considers them historical
Mark a past transaction as failed without drawSplitDetailsThe system needs draw allocation data to reverse the transaction's balance impact

System design constraints and known behaviors

These behaviors are by design but may surprise clients who expect the system to work differently than their legacy platform.

Grace period override during migration

If a draw is configured with numPeriodsToRestoreGrace greater than 1 (meaning the borrower must pay in full for multiple consecutive periods to regain grace), this rule is overridden to 1 during the migration period. Peach defaults to the borrower-favorable behavior because it does not have per-draw grace history for pre-migration periods. The configured numPeriodsToRestoreGrace takes full effect starting in the period after migration. See Appendix B: Grace Period for Lines of Credit for details.

DPD calculation for migrated loans

Days past due for migrated loans combines the migrated overdue days with the time elapsed since cutoff:

effectiveDPD = peachTimeDiff + maxMigratedDaysOverdue

Where peachTimeDiff is (today - migrationCutoffDate).days and maxMigratedDaysOverdue is the highest migratedDaysOverdue value across all obligations. This means DPD continues to increase after migration if the borrower doesn't pay — it doesn't reset to zero at cutoff.

As the borrower pays down the overdue balance, the migrated overdue remaining amount decreases:

migratedOverdueRemainingAmount = migratedOverdueAmount - amountPaid

When migratedOverdueRemainingAmount reaches zero, the migrated overdue component is fully resolved.

Warning: The DPD prorating is continuous and does not account for period-level overdue buckets. This can produce unexpected results when a borrower has overdue balances spanning multiple periods. For example, if a borrower has overdue balances of $100 from period 1 (90 days overdue), $100 from period 2 (60 days overdue), and $800 from period 3 (30 days overdue), and they pay $200, they might expect to be considered only 30 days overdue (having cleared the two oldest periods). However, the system pro-rates continuously: with 80% of the total overdue amount remaining, it calculates 80% × 90 = 72 days overdue. This behavior is on the roadmap to be improved, but it is the current system behavior.

No loan tapes for pre-migration periods

Peach does not generate loan tapes for periods before the migration cutoff date. After migration, loan tape fields are conditionally enabled based on whether the loan has a migration cutoff. This means historical loan tape data from before the cutoff must be sourced from your legacy system.

Past-transaction drawSplitDetails access

The drawSplitDetails field for past transactions (showing how a payment was allocated across draws) is only available through the migration-specific endpoint (GET .../migration/past-transaction). The standard GET .../transactions endpoint does not return this field. This is a common source of confusion when building post-migration reconciliation tools.

Balance precision and credit balance handling

All balance amounts must have a precision of exactly 0.01 (two decimal places) and be ≥ 0.00. Amounts with more than two decimal places are rejected. If a borrower has a credit balance (the lender owes the borrower money), use the reimbursementAmount field — do not use negative principal values.

Statement date must be endDate + 1

Peach enforces a hard constraint: the statement date must be exactly one day after the period's end date. If statementDate != endDate + 1 day, validation fails. This is worth noting because some legacy systems use different date conventions.

Effective time of day minimum

All transactions must have an effectiveTimeOfDay after 2:00 AM in the product's timezone. Transactions with earlier times fail validation. This constraint exists because DLM processes run in the early morning hours, and transactions must not conflict with those processes.

Overdue amount and days overdue consistency

The system validates that migratedDaysOverdue and migratedOverdueAmount are consistent:

  • Cannot have migratedDaysOverdue > 0 if migratedOverdueAmount is 0
  • Cannot have migratedOverdueAmount > 0 if migratedDaysOverdue is 0
  • Cannot have migratedOverdueAmount > 0 if the total obligationAmount is less than the statement's minimumAmountDue (this implies payments were made that should have reduced the overdue state)

Interest rounding for LOCs

At statement date, when non-due interest moves to due, the system truncates to the nearest cent. The fractional cents are moved to a forgone_interest_rounding bucket and are not recovered. Over many periods, this can create small discrepancies between Peach's interest calculation and a legacy system that rounds differently.


Origination and draw fees

If the LOC charges origination fees or draw fees that are expected to be paid after the migration cutoff date, configure them during loan and draw creation.

Origination fees

Pass the origination fee amount in the atOrigination.fees object when creating the loan:

{
  "atOrigination": {
    "fees": {
      "originationFeeAmount": 150.00
    }
  }
}

Then include the remaining origination fee balance in the migration period LOC data under nonDueBalances.nonDueOriginationFeesAmount, dueBalances.dueOriginationFeesAmount, or overdueBalances.overdueOriginationFeesAmount depending on the fee's current status.

Draw fees

Pass the draw fee amount in the draw's atOrigination.fees object:

{
  "atOrigination": {
    "fees": {
      "drawFeeAmount": 50.00
    }
  }
}

Include the remaining draw fee balance in the migration period draw data under the appropriate nonDueDrawFeesAmount, dueDrawFeesAmount, or overdueDrawFeesAmount field.


Statement and due date configuration

Peach supports three options for statement and due date scheduling on LOCs. The option you choose affects how you set specificDays, statementDate, and dueDate across all periods.

OptionHow it worksConfiguration
Fixed due dateDue date is fixed; statement date is calculated based on grace period days.Set specificDays to the due date day (e.g., [22]). Statement date is auto-calculated.
Fixed statement date (LOC only)Statement date is fixed; due date is calculated based on grace period days.Set specificDays to the statement date day. Due date is auto-calculated.
Fixed both (LOC only, monthly only)Both statement date and due date are fixed.Set specificDays to both days (e.g., [1, 22] where 1 is statement date, 22 is due date). Only works with paymentFrequency: "monthly".

Ensure that the specificDays in atOrigination align with the dueDate (or statementDate, depending on your configuration) in the migration period data.


Modifying historical data post-migration

For a complete reference on what you can and can't change after migration — including supported modifications, constraints, and code examples — see What you can and can't change after migration in Step 6.

The following are the API code examples for each supported modification type.

Dispute historical purchases

If a borrower disputes a purchase that occurred before the migration cutoff date:

POST /api/people/{personId}/loans/{loanId}/draws/{drawId}/purchases/{purchaseId}/disputes
Content-Type: application/json

{
  "reason": "unauthorized",
  "amount": 249.99
}

This creates a type: "refund" purchase with an effective date after the migration cutoff, which lowers the draw balance using the existing refund functionality.

Waive or cancel historical fees

Cancel a fee that was charged before the migration cutoff date:

PUT /api/people/{personId}/loans/{loanId}/fees/{feeId}/cancel

The system changes the fee status to canceled and depletes the fee balance. If the fee balance is already zero, it depletes draw principal instead.

Change historical transaction status

Update the status of a past transaction (e.g., marking a pending payment as succeeded or failed):

PUT /api/people/{personId}/loans/{loanId}/migration/past-transaction/{transactionId}
Content-Type: application/json

{
  "status": "failed"
}

Allowed transitions: pendingsucceeded, and pending/succeededfailed.

Warning: For transitions to failed, the system requires drawSplitDetails from the original transaction to know which draw balances to increase. These details are only available via the migration-specific endpoint (GET .../migration/past-transaction), not the standard transactions endpoint. If drawSplitDetails is null, the status change to failed will be rejected.

Reverse historical transactions

Reverse a past transaction to undo its effect:

POST /api/people/{personId}/loans/{loanId}/transactions/{transactionId}/reverse

The system creates a new reversal transaction. For each entry in the original drawSplitDetails, the system increases the corresponding draw's non-due principal balance.

Correct and re-create purchases or transactions

If you need to delete and re-create a purchase or transaction with the same external ID:

  • Purchases: Update the purchase with externalId: null to release the external ID, then create a new purchase with the original external ID.
  • Transactions: Cancel the transaction with clearExternalId: true to release the external ID, then create a new transaction with the original external ID.

Migrating special loan states

Not all loans are current when they migrate. Peach supports migrating loans that are delinquent, accelerated, or charged off.

Delinquent loans

For loans that are past due at the time of migration, set the migratedDaysOverdue and migratedOverdueFromDate fields in the migration period obligation data.

After migration, Peach dynamically prorates the migrated days overdue as the borrower makes payments:

Effective DPD = migratedDaysOverdue × (1 - min(fulfilledAmount / migratedOverdueAmount, 1))

This means that as the borrower pays down the overdue balance, the effective days past due decreases proportionally.

Warning: This prorating formula operates continuously across the total overdue amount and does not distinguish between overdue amounts from different periods. See the DPD prorating known limitation in the system design constraints section for a detailed example of how this can produce unexpected results with multi-period overdue balances.

Accelerated and charged-off loans

To migrate a loan directly into an accelerated or charged-off state, set postMigrationLoanStatus in the migration period LOC data:

{
  "postMigrationLoanStatus": "accelerated"
}

Or for charged-off:

{
  "postMigrationLoanStatus": "chargedOff",
  "chargedOffReason": "term"
}

Accepted chargedOffReason values: term, fraudulent, bankruptcy, legal

The effective date for the acceleration or charge-off is the migration cutoff date.

Special state migrations are most commonly used when onboarding a portfolio of delinquent loans onto Peach — for example, to leverage Peach's collections features for a portfolio where all loans are already past due. Ensure the chargedOffReason matches the reason from your legacy system.


Migration strategy and operations

For large portfolio migrations, follow these operational best practices.

Batching and segmentation

If migrating a large portfolio, segment it into batches. Start small and increase batch size as you gain confidence:

  1. Start with 50 loans — validate thoroughly
  2. Increase to 100, then 250, then 500 — monitor for patterns
  3. Scale to your target batch size once the process is stable

Group loans with similar attributes in each batch:

  • Current loans together
  • Delinquent loans together
  • Loans with promo programs together
  • Loans with specific fee configurations together

This makes it easier to identify reconciliation patterns. For example, if all loans in a "delinquent" batch show a consistent DPD offset, you can diagnose the root cause once and apply the fix to the entire batch.

There is no hard limit on batch size in production, but we recommend monitoring migration completion times and adjusting batch sizes based on your throughput requirements.

Borrower communications during migration

Set muteLoanNotices: true on loan creation to prevent notifications during migration. This avoids confusing borrowers with messages like payment reminders or balance alerts while data is still being set up.

Re-enable communications only after:

  1. Migration has succeeded
  2. Balances have been validated
  3. Autopay has been configured (if applicable)
  4. You're ready to sunset the legacy system's communications for that borrower

Concurrent system operation

During migration, both your legacy system and Peach can operate simultaneously. This allows for thorough testing and validation without disrupting your business.

Canceling and re-migrating loans

If a loan was migrated incorrectly and cannot be fixed with adjustments, you can cancel it and re-migrate. This is also the recovery path if you miss the migration period due date — the loan must be canceled and re-created with a new migration period aligned to your next statement date.

Option A — Clear all external IDs automatically (up to 1,000 objects):

POST /api/people/{personId}/loans/{loanId}/cancel?clearAllExternalIds=true

This clears external IDs on the loan, all its draws (including the migration draw, which is auto-canceled), transactions, purchases, and fees — up to 1,000 objects total. If your loan has more than 1,000 associated objects, use Option B.

Option B — Manual cleanup for large loans (> 1,000 objects):

  1. Cancel each draw individually, clearing its external IDs.
  2. Cancel the loan with clearAllExternalIds=true for the remaining objects.

After cancellation:

  1. Create a new loan with the original external ID and repeat the migration process.
  2. If you need to adjust data on the canceled loan before re-migrating (e.g., convert migration period data to a past period, reclassify live activities as past activities), you can update the canceled loan's data first.

Info: Canceled loans do not appear on loan tapes. The clearAllExternalIds parameter frees up external IDs for re-use on the corrected loan without requiring manual changes.


Validation rules and common errors

Peach validates migration data at multiple points. Understanding these rules helps prevent failures.

Period validation

RuleError symptom
No gaps between periodsstartDate of period N+1 must equal endDate of period N + 1 day
No overlapping start-end date rangesThe startDateendDate range of one period cannot overlap with another period's range
No duplicate datesTwo periods cannot share the same startDate, endDate, statementDate, or dueDate
Due date falls in next periodEach period's dueDate must fall between the next period's startDate and endDate

Transaction validation

RuleError symptom
effectiveTimeOfDay must be after 2:00 AMTransactions with earlier times fail validation
External transactions require isExternal: trueLive transactions processed by legacy system must be flagged as external
Historical transactions use the past-transaction endpointDon't use the standard transaction endpoint for pre-cutoff activity

Balance validation

RuleError symptom
All amounts ≥ 0.00Negative amounts are rejected
Precision of 0.01Amounts with more than 2 decimal places are rejected
Credit balances use reimbursementAmountDon't use negative principal for credit balances
Overdue balances must match obligationsIf the obligation states the borrower is overdue by a certain amount, the overdue balance amounts across draws must add up to that amount. Mismatches will cause migration to fail.

Troubleshooting and FAQ

Balance discrepancies

Q: Why doesn't my balance match the legacy system?

Balance discrepancies after migration almost always trace back to one of the following root causes. Work through this diagnostic sequence:

Step 1 — Compare total balance first. If the total outstanding balance (principal + interest + fees) matches but the breakdown doesn't, skip to "Principal/interest split differences" below. If the total doesn't match, continue.

Step 2 — Check balance timing. Balances must be provided as of the migration cutoff date. The codebase enforces a specific rule: balances should include interest accrued through the last day of the previous period (endDate) but should NOT include interest for the statement date (migration period start date). If you included an extra day of interest, you'll see a small positive discrepancy.

Step 3 — Check day count conventions. Peach accrues interest for the actual number of days in each month. Some legacy systems use a 30-day month convention (30/360). This means:

MonthPeach (actual days)30/360 systemDifference per $10,000 at 18% APR
February (non-leap)28 days30 days-$0.99
February (leap)29 days30 days-$0.49
March31 days30 days+$0.49

Over a full year these differences partially cancel out, but at any given cutoff date there may be a discrepancy of a few dollars.

Step 4 — Check rounding behavior. Peach accrues interest daily with high precision, then truncates to the nearest cent at statement date. Fractional cents go to a forgone_interest_rounding bucket. If your legacy system rounds at a different cadence (e.g., per-period rather than per-day), small cumulative rounding differences are expected.

Step 5 — Break down by draw. Compare balances at the draw level. If one specific draw is off, check that draw's interest rate, promo rate, and fee configuration. A mismatched rate will compound over the post-cutoff period.

Minor discrepancies (typically < $1.00) are normal and can be addressed with service credits or adjustment fees. See Step 6: Post-migration → Adjust balances.

Principal/interest split differences

Q: Why is the principal/interest split different from my legacy system?

Peach replays live activity chronologically and applies its own interest calculation rules. Common causes of split differences:

  • Accrual method: Peach uses daily accrual with actual/365 day counting. If your legacy system uses 30/360 or periodic (monthly) accrual, the interest component will differ.
  • Interest truncation at statement date: For LOCs, when non-due interest moves to due at statement date, the system truncates to the nearest cent. The fractional amount goes to forgone_interest_rounding. If your legacy system doesn't truncate at this step, interest will accumulate differently.
  • Grace period retroactive accrual: If a borrower is in grace and makes a payment between statement date and due date, Peach backdates the payment's effectiveDate to the statement date for interest calculation purposes. If your legacy system applies the payment at the actual date, the interest calculation will differ for that period.

The total balance should still match within tolerance — if it doesn't, check the balance timing and day count rules above.

Interest calculation after grace period revocation

Q: Why did the borrower get a large interest charge after migration?

If the borrower was in grace before migration but the grace period was revoked post-migration (because they didn't pay in full by the due date), Peach accrues interest retroactively on the unpaid portion from the statement date, not from the due date. This is by design (see Appendix B: Grace Period for Lines of Credit) but can produce a larger interest charge than expected if the borrower had been in grace for a long period.

Check: Was isGracePeriodEligible set correctly in the migration period data? If it was set to true but the borrower shouldn't have been in grace, the system will have suppressed interest accrual during the migration period and then retroactively accrued when grace was revoked — creating a sudden lump-sum charge.

Loan tapes after migration

Q: How do migrated loans appear on loan tapes?

Peach does not generate loan tapes for periods before migration. After migration, the system generates loan tapes based on your tape configuration. Migrated loans appear on tapes as if they are any other loan. Canceled loans do not appear on loan tapes. Loan tape fields are conditionally enabled based on whether the loan has a migration cutoff — some fields that depend on full loan history may not be available for migrated loans.

Migration processing time

Q: How long does migration take?

Processing time depends on the number of draws, historical activities (purchases, transactions, fees), and batch size.

ModeBehaviorRecommended for
Synchronous (sync=true)Blocks until migration completes or 60-second timeoutSingle loans, testing
Asynchronous (default)Returns immediately, processes in backgroundProduction, batches

If sync mode times out (HTTP 408), the migration is still processing — check migrationStatus via GET /loans/{loanId}. A single loan with a few draws and modest history typically migrates in seconds. Loans with many draws and extensive transaction history may take minutes.

For large batches, always use async mode. Monitor progress by polling loan status or subscribing to the loan.migration.succeeded and loan.migration.failed webhook events.

Failed migration with no errors

Q: Migration status shows failed but I don't see specific errors. What do I do?

Contact Peach Support. Some failure scenarios (e.g., internal timeouts during replay) don't surface user-facing errors. Support can investigate the migration event logs.

When migration fails, the system automatically:

  1. Rolls back the loan status to pending
  2. Clears the migrated_at timestamp
  3. Reverses all ledger entries created during the migration attempt
  4. Resets the migration status to failed

You can then reset to prepMigration, correct any data issues, and attempt migration again.

Re-using external IDs after cancellation

Q: I canceled a migrated loan but can't create a new loan with the same external ID.

External IDs must be unique across all loans in your company, including canceled ones. Use clearAllExternalIds=true when canceling (see Canceling and re-migrating loans), or change the external ID on the incorrectly migrated loan before canceling (e.g., append -canceled).

Sandbox testing

Q: Can I test the full migration workflow in sandbox?

Yes. Sandbox supports the complete migration workflow. Limit testing to 5 loans at a time — larger batches may cause processing delays or aborted migrations.

Migration validation errors

Q: What does "Loan is missing ledger update event" mean?

The POST /migrate endpoint validates that every non-static draw has a corresponding ledger update event (created when you set up the migration period data). This error means you created a draw but didn't create migration period data for it. Create the migration period draw data (POST .../draws/{drawId}/migration/period) for the missing draw, then retry migration.

Q: What does "Operation would lead to periods overlapping" mean?

The period validation checks that no two periods have overlapping start-end date ranges, that there are no gaps between consecutive periods, and that statementDate = endDate + 1 day. Review your past periods data and migration period data for date inconsistencies. Common causes: daylight saving time boundaries, months with different day counts, or off-by-one errors in date calculations.


Webhook events

Peach does not fire webhooks during the migration process itself. After migration completes successfully, webhooks begin firing for subsequent loan activity.

EventWhen it fires
loan.migration.succeededMigration completed successfully
loan.migration.failedMigration failed and was rolled back
Events for live activitiesAfter successful migration, webhooks fire for all events created during replay of live activities

API quick reference

ActionMethodEndpoint
Create borrowerPOST/api/people
Create contactPOST/api/people/{personId}/contacts
Create payment instrumentPOST/api/people/{personId}/payment-instruments
Create loan (LOC)POST/api/people/{personId}/loans
List drawsGET/api/people/{personId}/loans/{loanId}/draws
Create drawPOST/api/people/{personId}/loans/{loanId}/draws
Create documentPOST/api/people/{personId}/documents
Upload document contentPOST/api/people/{personId}/documents/{docId}/content
Create past periodsPOST/api/people/{personId}/loans/{loanId}/migration/past-periods
Get past periodsGET/api/people/{personId}/loans/{loanId}/migration/past-periods
Update past periodPUT/api/people/{personId}/loans/{loanId}/migration/past-periods
Delete past periodDELETE/api/people/{personId}/loans/{loanId}/migration/past-periods
Create migration period (LOC)POST/api/people/{personId}/loans/{loanId}/migration/period
Get migration period (LOC)GET/api/people/{personId}/loans/{loanId}/migration/period
Update migration period (LOC)PUT/api/people/{personId}/loans/{loanId}/migration/period
Create migration period (draw)POST/api/people/{personId}/loans/{loanId}/draws/{drawId}/migration/period
Get migration period (draw)GET/api/people/{personId}/loans/{loanId}/draws/{drawId}/migration/period
Update migration period (draw)PUT/api/people/{personId}/loans/{loanId}/draws/{drawId}/migration/period
Update loan statusPUT/api/people/{personId}/loans/{loanId}
Create purchasePOST/api/people/{personId}/loans/{loanId}/draws/{drawId}/purchases
Create past transactionPOST/api/people/{personId}/loans/{loanId}/migration/past-transaction
Get past transactionsGET/api/people/{personId}/loans/{loanId}/migration/past-transaction
Update past transactionPUT/api/people/{personId}/loans/{loanId}/migration/past-transaction/{txnId}
Create transactionPOST/api/people/{personId}/loans/{loanId}/transactions
Create feePOST/api/people/{personId}/loans/{loanId}/fees
Migrate loanPOST/api/people/{personId}/loans/{loanId}/migrate
Get loan (check status)GET/api/people/{personId}/loans/{loanId}
Get balanceGET/api/people/{personId}/loans/{loanId}/balance
Create autopayPOST/api/people/{personId}/loans/{loanId}/autopay
Cancel feePUT/api/people/{personId}/loans/{loanId}/fees/{feeId}/cancel
Reverse transactionPOST/api/people/{personId}/loans/{loanId}/transactions/{txnId}/reverse
Create purchase disputePOST/api/people/{personId}/loans/{loanId}/draws/{drawId}/purchases/{purchaseId}/disputes
Cancel loanPOST/api/people/{personId}/loans/{loanId}/cancel

Appendix A: LOC Billing Cycle Mechanics {#appendix-a-loc-billing-cycle-mechanics}

A line of credit billing cycle is fundamentally different from an installment loan. Understanding this structure is critical for migration because Peach must initialize balance buckets correctly at cutover — and post-migration, these mechanics govern how balances move, when interest accrues, and when a borrower becomes overdue.

Billing period structure

Every LOC billing cycle is divided into periods defined by four key dates:

DateWhat happens
startDatePeriod begins. New balance tracking starts.
endDatePeriod ends. Last day of the billing period.
statementDateStatement generated. Always endDate + 1. Non-due balances move to due.
dueDatePayment deadline. Full balance must be paid by this date for grace (if applicable).
dueDate + 1Enforcement. Unpaid due amounts move to overdue. Grace eligibility checked.

Warning: In Peach, statementDate must always equal endDate + 1 day. This is a hard validation rule — if they don't match, migration will fail with "Period statement date should be one day after end date."

Balance movement: non-due → due → overdue

Unlike installment loans where a single payment obligation comes due on one date, LOCs move balances through buckets on specific dates.

Non-due to due (statement date)

Between startDate and statementDate, all accrued interest and posted transactions sit in the non-due bucket. On statementDate, the system moves the remaining non-due balance to the due bucket. The amount moved equals the remainingAmount from the obligation for that period.

During this step, outstanding non-due interest is truncated to the nearest cent. Any fractional cents are moved to a forgone_interest_rounding bucket and are not recovered. This is a key difference from systems that round interest at a different cadence.

Due to overdue (due date + 1)

After statementDate, the balance sits in the due bucket. For LOCs, this is typically 2–3 weeks (dueDate - statementDate + 1 days). On dueDate + 1, any remaining due balance moves to overdue.

Loan typeNon-due → dueTime in "due"Due → overdue
Line of creditStatement date2–3 weeksDue date + 1
InstallmentDue date1 dayDue date + 1

This is the most important structural difference between LOCs and installment loans. For installment loans, balances move from non-due to due on the due date and become overdue just one day later. For LOCs, there's a substantial window between "due" and "overdue."

Obligations: tracking what the borrower owes

An obligation tracks how much the borrower must pay in a specific billing period. It answers: "How much is owed, how much has been paid, and how much remains?"

The obligation amount is computed from expected payments — forecasts of what the borrower should pay based on the loan's schedule and fee structure:

obligationAmount = sum(expectedPayment.amount)
  where expectedPayment.period_id = obligation.period_id

Expected payments include: one PeriodicPayment (principal + interest), any number of DynamicFee entries (e.g., periodic servicing fees), and optionally one OriginationFee or DrawFee.

The obligation also tracks payment progress:

fulfilledAmount = paymentsAmount + serviceCreditsAmount
remainingAmount = obligationAmount - fulfilledAmount

The fulfilledAmount calculation respects the allowPrepayments loan type setting. If prepayments are allowed, a borrower's early payment can satisfy future obligations. If prepayments are not allowed, excess payments are tracked as overpaymentsAmount but don't reduce future remainingAmount.

The refresh_obligations() function recomputes all obligations whenever a relevant event occurs (payment, service credit, refund, etc.).

Why this matters for migration

During migration, you must seed Peach's ledger with the correct balance buckets. Your legacy system's balances must be broken down into non-due, due, and overdue — per fee type (principal, interest, periodic fees, late fees, origination fees, draw fees, etc.). If these breakdowns are incorrect, post-migration DLM will move the wrong amounts at statement date, causing balance discrepancies.

You also need to provide the correct obligationAmount for the migration period (reconstructed from your legacy system's billing data). For overdue loans, you must include migratedDaysOverdue and migratedOverdueAmount so that Peach can correctly track the borrower's delinquency status going forward.

Timeline example

Day 1 (Aug 1):    startDate — Period begins
                   ├─ Interest accrues daily in non-due bucket
                   ├─ Purchases post to non-due bucket
                   └─ Borrower can make payments (reduce non-due)

Day 31 (Aug 31):  endDate — Period ends

Day 32 (Sep 1):   statementDate — Statement generated
                   ├─ Non-due balance moves to due bucket
                   ├─ Interest truncated (fractional cents → forgone_interest_rounding)
                   ├─ Obligation sealed (obligationAmount set)
                   └─ Statement mailed/emailed to borrower

Days 32–53:       Due period (balance in "due" for ~3 weeks)
                   ├─ Borrower makes payments (reduce due)
                   └─ Grace: payments effectiveDate → statementDate if eligible

Day 53 (Sep 22):  dueDate — Payment deadline
                   └─ Last day to pay for grace eligibility

Day 54 (Sep 23):  dueDate + 1 — Enforcement
                   ├─ Remaining due balance moves to overdue
                   ├─ Grace eligibility checked (revoke or reinstate)
                   ├─ loan_overdue event fires (if overdue)
                   ├─ Late fees may be charged
                   └─ Acceleration/charge-off threshold checked

Appendix B: Grace Period for Lines of Credit {#appendix-b-grace-period-for-lines-of-credit}

Grace periods allow borrowers to avoid paying interest when they pay their full statement balance by the due date. Understanding grace is essential for migration because incorrectly initialized grace status will cause Peach to accrue interest when it shouldn't — or fail to accrue interest when it should.

What grace period means

A grace period is a window during which no interest accrues on a balance, provided the borrower pays the full statement balance by the due date. This is a regulatory concept common in credit cards.

The rules are:

  • Pay in full by due date → grace continues; no interest on that balance for the period
  • Don't pay in full → grace is lost; interest accrues retroactively on the unpaid portion from the statement date (not the due date)
  • To restore grace → pay in full for numPeriodsToRestoreGrace consecutive periods

Daily grace tracking (isGracePeriodEligible)

Grace is tracked via a boolean flag (isGracePeriodEligible) on each obligation. DLM checks this flag every day to decide whether to accrue interest:

if draw.isGracePeriodApplicable == true
  AND obligation.isGracePeriodEligible == true
  → DO NOT accrue interest for this day
else
  → accrue interest normally

The system carries the prior day's grace status forward as a default and updates it on dueDate + 1 (see below).

The due date + 1 check

The critical decision point for grace is one day after the due date. This is when Peach evaluates whether the borrower paid in full, and either revokes or reinstates grace.

If the borrower paid in full (fulfilledByDueDateAmount >= fullBalanceAmount):

  • If already in grace → no change (grace continues)
  • If not in grace → check the past numPeriodsToRestoreGrace - 1 obligations. If all of them were also paid in full, reinstate grace: set isGracePeriodEligible = true and trigger a replay from period.startDate. The replay retroactively removes interest that was accrued while grace was ineligible.

If the borrower did NOT pay in full and the obligation was grace-eligible:

  • Revoke grace: set isGracePeriodEligible = false and trigger a replay from period.startDate. The replay retroactively accrues interest on the unpaid portion from the statement date forward.

Warning: When grace is revoked, the retroactive interest charge can be large if the borrower had been in grace with a high balance. The interest accrues on the unpaid balance from the statement date, not the due date. Make sure borrower communications explain this behavior.

numPeriodsToRestoreGrace and the migration override

After grace is revoked, the borrower must pay in full for N consecutive periods to regain eligibility. This is controlled by numPeriodsToRestoreGrace (typically configured on the draw or loan type).

Migration override: For the migration previous period, Peach overrides numPeriodsToRestoreGrace to 1 regardless of the configured value. This means a borrower who lost grace before migration only needs to pay in full for one period post-migration to restore it. The override exists because Peach does not have per-draw grace history for pre-migration periods and defaults to the behavior most favorable to the borrower.

The configured numPeriodsToRestoreGrace takes full effect starting in the period after migration.

Transaction effective date manipulation

When a payment is applied between statementDate and dueDate on a draw that is grace-eligible, Peach manipulates the transaction's effective date:

transaction.effectiveDate = obligation.statementDate  (backdated)
transaction.displayDate = actual_payment_date          (for customer statements)
→ replay from period.startDate

This ensures that the interest calculation treats the payment as if it was made on the statement date. Without this, a payment on due date - 5 would leave 5 days of interest unaddressed in the calculation.

Grace adjustment for refunds and cashbacks

If a refund or cashback is issued between the statement date and the due date, it reduces the amount the borrower needs to pay to maintain grace:

effectiveFullBalance = fullBalanceAmount - adjustmentAfterStatementAmount

For example: if the statement balance is $1,000 and a $200 merchant refund is issued before the due date, the borrower only needs to pay $800 by the due date to keep grace.

Loan-level vs. draw-level grace

Grace can be tracked at two levels:

LevelConfigurationBehavior
Loan-levelloc.isGracePeriodApplicable = trueAll draws share one grace status. If any draw loses grace, the entire LOC loses grace.
Draw-leveldraw.atOrigination.gracePeriod.isGracePeriodApplicable = trueEach draw tracks grace independently.

During migration, the system sets loan-level grace based on the aggregate of all draw-level grace statuses: if all draws are grace-eligible, the LOC is grace-eligible.

Why this matters for migration

  1. Set isGracePeriodEligible correctly in the migration period data. If the borrower was in grace before migration, set to true. If they had lost grace, set to false.
  2. Record the grace balance (gracePeriod.fullBalanceAmount and gracePeriod.fulfilledByDueDateAmount) accurately. These determine whether the due date + 1 check revokes or reinstates grace.
  3. If grace is false, understand that post-migration interest will accrue retroactively from the statement date of the migration period — which may create a significant interest charge on the first post-migration statement.
  4. The migration override (numPeriodsToRestoreGrace = 1) means borrowers who lost grace before migration can regain it after just one on-time full payment post-migration, even if the loan type normally requires multiple consecutive periods.

Appendix C: LOC Draws — How They Work

A draw is a sub-account within a line of credit. Each draw has its own balance, interest rate, fee structure, and transaction history. Draws allow borrowers to segregate different types of borrowing (e.g., purchases at one rate, cash advances at a higher rate) within a single credit line.

What is a draw?

A draw is a virtual "bucket" within the LOC. When a purchase is posted or a cash advance is taken, it is assigned to a specific draw. The LOC's total credit limit is shared across all active draws, but each draw independently maintains its own balance, interest rate, promotional rate, draw fees, and minimum payment calculation.

Draw types

Peach supports four draw types:

Draw typePurposeTypical useInterest behavior
RegularPurchaseStandard retail purchasesPrimary draw type for card activityStandard APR
CashAdvanceCash withdrawal or equivalentATM withdrawals, cash-like transactionsOften higher APR; may have different fees
BalanceTransferTransferred balance from another cardPromotional offersOften 0% APR promotional rate
StaticMigration draw (historical data)Created automatically at migrationNo interest accrual; no new activity after migration

Most active LOCs have multiple draws. The Static draw is special and used only during migration.

How draws share the credit limit

The LOC has a single credit limit (e.g., $10,000) that applies across all draws:

available_credit = loc.creditLimit - sum(draw.balance for all active draws)

Each draw can also have a sub-limit that further restricts that specific draw type. Both the LOC-level limit and the draw-level limit must be satisfied for a new purchase to be approved.

Draw fees

Fees can be charged at draw creation or throughout the draw's lifecycle:

Fee typeWhen chargedExample
Draw feeAt draw creation$75 cash advance origination fee
Transaction feePer transaction3% foreign transaction fee
Periodic feeEach billing period$5/month servicing fee
Late feeWhen draw goes overdueAutomatic, handled by DLM

Draw fees are tracked separately from LOC-level fees (like annual fees). During migration, draw fee balances must be included in the migration period draw data under the appropriate nonDueDrawFeesAmount, dueDrawFeesAmount, or overdueDrawFeesAmount fields.

How purchases are associated with draws

When a purchase is posted, it must be assigned to a specific draw. The assignment determines which draw's balance increases, which interest rate applies, and which minimum payment calculation is used.

During migration, historical purchases are posted to the migration draw (static draw) with an originalDrawId field referencing the actual legacy draw they belonged to. Post-migration, new purchases are posted directly to the appropriate draw (RegularPurchase, CashAdvance, etc.).

Static draw (migration draw)

The static draw is an internal construct created at migration to hold all pre-migration transactions and balances. It exists solely to bootstrap the LOC with historical data.

Creation: Peach automatically creates one static draw per LOC when the loan is created with migrationStatus: "prepMigration". You don't create it manually.

Configuration:

PropertyValueWhy
drawTypeStaticIdentifies it as the migration draw
timestampsEqual to LOC activation dateAppears to have existed since LOC origination
min_payment_percentage_of_principal0Static draw doesn't contribute to minimum payment
migration_statusSynced with LOCFollows the LOC's migration lifecycle

What it holds: All pre-migration purchases, transactions (payments, service credits), fees, and obligations.

Behavior after migration: The static draw transitions to Active status (like all other draws) but is effectively disabled — the API rejects any attempt to post new activity to a static draw with "This endpoint is invalid for a static draw." Payment application can still target the static draw if the borrower carries an unpaid balance from pre-migration.

Warning: The originalDrawId field on historical purchases references the actual legacy draw, not the static draw. This lets you track which legacy draw each purchase belonged to, even though all historical purchases are stored under the static draw in Peach.

Minimum payment calculation

The LOC's minimum payment is the sum of each draw's minimum payment:

loc.minimumPayment = sum(draw.principal * draw.minPaymentPercentage for all active draws)

The percentage can vary by draw type (e.g., CashAdvance might require 5% while RegularPurchase requires 2%). The static draw's percentage is 0, so it contributes nothing to the minimum payment. This prevents borrowers from facing unexpectedly high minimums immediately after migration due to old balances.

Why this matters for migration

  1. The static draw is created automatically — don't create it manually.
  2. Post all historical purchases to the static draw using originalDrawId to reference the actual legacy draw.
  3. Create actual draws (RegularPurchase, CashAdvance, etc.) for any draw types that were active before migration and should continue to accept new activity.
  4. Populate migration period data for each draw — the system validates that every non-static draw has a ledger update event.
  5. After migration, all new activity goes to actual draws. The static draw is read-only.
  6. Draw fees must be configured correctly on each draw's atOrigination.fees object, and fee balances must be seeded in the migration period draw data.