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.
Before diving into the steps, it helps to understand how LOC migration compares to installment loan migration at a high level.
| Aspect | Installment loan | Line of credit |
|---|---|---|
| History handling | Full replay from activation date | Read-only history before cutoff; replay after |
| Object structure | Single loan object | Line (loan) + multiple draws + migration draw |
| Migration timing | Any time | Between statement date and due date |
| Balance calculation | Fully recalculated from scratch | Seeded from statement balances |
| Historical objects | Expected payments | Past periods, purchases, past transactions (optional — see note below) |
| Replay behavior | All events replayed chronologically | Only 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.
These terms appear throughout the guide. Understanding them before you start will make each step clearer.
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.
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.
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
originalDrawIdfield 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
activestatus 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.
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.
Activity is categorized based on when it occurred relative to the migration cutoff date:
| Historical activity | Live activity | |
|---|---|---|
| When it occurred | Before migration cutoff date | On or after migration cutoff date |
| Where purchases go | Migration draw (with originalDrawId) | Actual draw |
| Transaction endpoint | POST .../migration/past-transaction | POST .../transactions (with isExternal: true) |
| Fee endpoint | POST .../fees (with migration.originalDrawId) | POST .../fees (normal) |
| Replayed by Peach? | No | Yes |
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.
Complete these steps before beginning the migration process.
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.
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.
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.
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
Migration must be executed within a specific window:
- The earliest you can migrate is immediately after the migration cutoff date (your most recent statement date).
- 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).
Establish the borrower's identity and payment capabilities in Peach.
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 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 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.
Create the loan object, retrieve the auto-generated migration draw, create any additional active draws, and upload loan documents.
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:
| Field | Description |
|---|---|
paymentFrequency / specificDays | Current 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]. |
interestRates | Interest rate schedule. Set from the migration cutoff date. Use days: null for an indefinite rate. |
promoRates | Promotional rate schedule, if applicable |
creditLimitAmount | Credit limit at origination |
gracePeriod | Grace period configuration. numDays is the number of days after the statement date during which no interest accrues if the full balance is paid. |
minPaymentCalculation | Rules for calculating the minimum payment each cycle |
skipCreditReporting | Set to true if you do not report this loan to credit bureaus |
Key fields in migration:
| Field | Description |
|---|---|
migrationStatus | Must be "prepMigration" to begin the process |
activatedDate | The original date the line of credit was opened in your legacy system |
activatedTimeOfDay | Time of activation (hour, minute, second in product timezone) |
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}/drawsResponse (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 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 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.pdfIf you're providing historical statements for past periods (Step 3), save each document descriptor ID — you'll reference it when creating past period 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.
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
startDateof each period must equal the day after the previous period'sendDate. - The
statementDatefor each period must equal the day after the period'sendDate. - The
dueDatemust 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:
| Field | Description |
|---|---|
fullBalanceAmount | Total balance eligible for grace period evaluation |
fullBalanceMinusOverdueAmount | Full balance minus any overdue amount |
totalFulfilledOnDueDateAmount | Amount paid by the due date. Set this on the period one day after the due date. |
isGracePeriodEligible | Whether the borrower maintained grace eligibility through this period |
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.
| Field | Description |
|---|---|
creditLimitAmount | Current overall credit limit for the line |
reimbursementAmount | Amount the lender owes to the borrower (credit balances). Set to 0.00 if not applicable. |
obligation.obligationAmount | Remaining amount the borrower must pay by the upcoming due date |
obligation.migratedDaysOverdue | Number of days the account was overdue as of the cutoff date. Set to 0 if current. |
obligation.migratedOverdueFromDate | Date 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).
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.
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.
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.
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.
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"
}
}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 paymentserviceCredit— Credit applied to the account (note: you cannot passstatusfor service credits — the system automatically sets it tosucceeded)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. drawSplitDetailsare only returned by theGET .../migration/past-transactionendpoint, not the standardGET .../transactionsendpoint.
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"
}
}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
typefield differs between endpoints: the past-transaction endpoint usesoneTimePayment,serviceCredit, anddownPayment, while the standard transaction endpoint usesoneTime,serviceCredit, etc. Use the correct type value for the endpoint you're calling. - The
effectiveTimeOfDaymust be after 2:00 AM in the product timezone. Transactions with earlier times will fail validation.
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 type | Additional migration field | Description |
|---|---|---|
| Late fees, annual fees | migration.originalDrawId | Associates with a legacy draw. Omit for line-level fees. |
| NSF fees | migration.originalTransactionId | Associates with the failed transaction |
| Foreign transaction fees | migration.originalPurchaseId | Associates with the purchase that triggered the fee |
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
}
}With all data in place, trigger the migration process. Peach validates the prepared data, replays live activities, and activates the loan.
POST /api/people/{personId}/loans/{loanId}/migrate
Content-Type: application/json
{
"sync": false
}| Parameter | Description |
|---|---|
sync: true | Synchronous — 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: false | Asynchronous — the API call returns immediately and migration runs in the background. Recommended for production and bulk migrations. |
For asynchronous migrations, poll the loan endpoint to check progress:
GET /api/people/{personId}/loans/{loanId}Watch the migrationStatus field:
| Status | Meaning |
|---|---|
prepMigration | Data preparation phase (pre-migrate call) |
migrating | Migration in progress — Peach is replaying live events |
completed | Migration completed successfully |
failed | Migration failed — data has been rolled back |
When you call the migrate endpoint, Peach performs the following sequence:
- Validates all migration data (periods, balances, transactions, draws).
- Locks the loan to prevent concurrent modifications.
- Originates the loan if it's in
pendingstatus. - Sets line-level grace period information from the migration period data.
- Activates the LOC and updates draw information for the migration period.
- Generates daily ledger management events.
- Replays all live activities (purchases, transactions, fees) in chronological order.
- Sets
migrationStatustocompleted. - Fires a
loan.migration.succeededwebhook event. - Fires webhooks for all events created for live activities.
- The loan status progresses from
pending→originated→active. migrationStatuschanges tocompleted.- A
loan.migration.succeededevent 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.
- The system rolls back the ledger — no partial state is left.
- The loan status reverts to
pending. migrationStatuschanges tofailed.- A
loan.migration.failedevent is created and a webhook fires.
To retry after a failure:
- Review the error details in the migration event.
- Correct the issue (fix data, adjust balances, etc.).
- Reset
migrationStatustoprepMigration:
PUT /api/people/{personId}/loans/{loanId}
Content-Type: application/json
{
"migration": {
"migrationStatus": "prepMigration"
}
}- Make any necessary data corrections.
- Call the migrate endpoint again.
Info: If migrationStatus shows failed but you don't see specific errors, contact Peach Support for investigation.
After a successful migration, validate the results, adjust any discrepancies, and configure ongoing features.
Compare the following values between Peach and your legacy system:
- Total outstanding balance (principal + interest + fees)
- Due amounts (minimum payment due)
- Overdue amounts and days past due
- Credit limit and available credit
- Individual draw balances
- Next payment due date and amount
- Interest rate(s) and any active promo rates
- Payment history accuracy
- 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-paymentsIf 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.
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.
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).
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:
- Payment Rating and History — Payment History Profile character definitions and calculation algorithm (including migration history handling)
- Field Specifications — K2 Segment (Purchased From) and L1 Segment (Account Number Change) field details
- Account Status Codes — How Peach determines status codes post-migration
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"
}
}
}
}| Field | Description |
|---|---|
paymentHistoryProfile | 24-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. |
dateOfFirstDelinquency | FCRA 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.purchasedFromName | Name 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.changeIndicator | Indicates what changed during migration. "1" = Consumer Account Number changed only. "2" = Identification Number changed only. "3" = both changed. See Field Specifications — L1 Segment. |
l1Segment.oldConsumerAccountNumber | The 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.oldIdentificationNumber | The 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:
| Character | Meaning |
|---|---|
0 | Current (0–29 days past due) |
1 | 30–59 days past due |
2 | 60–89 days past due |
3 | 90–119 days past due |
4 | 120–149 days past due |
5 | 150–179 days past due |
6 | 180+ days past due |
B | No payment history available for this month |
D | No payment due (e.g., forbearance, bankruptcy) |
E | Zero balance / current (open-ended accounts) |
L | Charge-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.
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.
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 status | DLM runs? | Interest accrues? |
|---|---|---|
active | Yes | Yes |
accelerated | Yes | No |
chargedOff | No | No |
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.
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.
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.
After migration, the way you create activity changes:
| Activity | During migration | After migration |
|---|---|---|
| Purchases | Posted to migration draw with originalDrawId | Posted to actual draws (POST /draws/{drawId}/purchases) |
| Transactions | POST .../transactions with isExternal: true | POST .../transactions (standard — no isExternal flag) |
| Fees | POST .../fees with migration.originalDrawId | POST .../fees (standard) |
| Payment instruments | isExternal: true for historical instruments | Standard 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."
The first post-migration billing cycle follows the standard LOC balance movement timeline:
- On statement date: Non-due balances move to due. The amount moved equals the
remainingAmountfrom the obligation for that period. Interest is truncated at this step — any fractional cents are moved to aforgone_interest_roundingbucket. See Appendix A: LOC Billing Cycle Mechanics for details. - 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
effectiveDateset to the statement date for interest calculation purposes. See Appendix B: Grace Period for Lines of Credit. - On due date + 1: Any remaining due balance moves to overdue. The system fires
loan_overdueevents, checks grace period eligibility, and evaluates whether the loan qualifies for acceleration or charge-off based on the loan type's overdue day threshold.
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.
| Action | How | Notes |
|---|---|---|
| Dispute a historical purchase | POST .../draws/{drawId}/purchases/{purchaseId}/disputes | Creates a type: "refund" purchase with an effective date after the cutoff. Lowers draw balance using standard refund functionality. |
| Waive or cancel a historical fee | PUT .../fees/{feeId}/cancel | Changes fee status to canceled and depletes the fee balance. If fee balance is already zero, depletes draw principal instead. |
| Change a past transaction's status | PUT .../migration/past-transaction/{txnId} | Allowed transitions: pending → succeeded, and pending/succeeded → failed. For transitions to failed, the system uses drawSplitDetails from the original transaction to increase draw balances. |
| Reverse a historical transaction | POST .../transactions/{txnId}/reverse | Creates a reversal transaction. For each entry in drawSplitDetails, increases the corresponding draw's non-due principal balance. |
| Correct and re-create with same external ID | Clear the external ID on the original object, then create a new one | For purchases: update with externalId: null. For transactions: cancel with clearExternalId: true. |
| Apply balance adjustments | Service credits (POST .../transactions) or adjustment fees (POST .../fees) | Use for post-validation balance discrepancies. See Adjust balances. |
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.
drawSplitDetailsrequired for past-transaction status changes tofailed. When marking a past transaction asfailed, the system needs to know which draws to increase balances for. IfdrawSplitDetailsis null (which can happen for transactions that pre-date the migration cutoff), the status change tofailedwill be rejected.drawSplitDetailsonly available via migration endpoint. Past-transactiondrawSplitDetailsare returned byGET .../migration/past-transaction, not by the standardGET .../transactionsendpoint. 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-periodscannot 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 withpostMigrationLoanStatus: "accelerated"orpostMigrationLoanStatus: "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
prepMigrationstatus.
| Action | Why |
|---|---|
| Post new activity to the migration draw | Static draws are disabled after migration |
| Modify past period data | Past periods are read-only snapshots |
Change migrationStatus back to prepMigration | Migration status can only move forward in the lifecycle |
Reverse accel/charge-off for loans created with postMigrationLoanStatus | These status changes preceded the migration cutoff; the system considers them historical |
Mark a past transaction as failed without drawSplitDetails | The system needs draw allocation data to reverse the transaction's balance impact |
These behaviors are by design but may surprise clients who expect the system to work differently than their legacy platform.
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.
Days past due for migrated loans combines the migrated overdue days with the time elapsed since cutoff:
effectiveDPD = peachTimeDiff + maxMigratedDaysOverdueWhere 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 - amountPaidWhen 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.
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.
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.
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.
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.
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.
The system validates that migratedDaysOverdue and migratedOverdueAmount are consistent:
- Cannot have
migratedDaysOverdue > 0ifmigratedOverdueAmountis 0 - Cannot have
migratedOverdueAmount > 0ifmigratedDaysOverdueis 0 - Cannot have
migratedOverdueAmount > 0if the totalobligationAmountis less than the statement'sminimumAmountDue(this implies payments were made that should have reduced the overdue state)
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.
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.
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.
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.
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.
| Option | How it works | Configuration |
|---|---|---|
| Fixed due date | Due 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.
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.
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.
Cancel a fee that was charged before the migration cutoff date:
PUT /api/people/{personId}/loans/{loanId}/fees/{feeId}/cancelThe system changes the fee status to canceled and depletes the fee balance. If the fee balance is already zero, it depletes draw principal instead.
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: pending → succeeded, and pending/succeeded → failed.
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 a past transaction to undo its effect:
POST /api/people/{personId}/loans/{loanId}/transactions/{transactionId}/reverseThe system creates a new reversal transaction. For each entry in the original drawSplitDetails, the system increases the corresponding draw's non-due principal balance.
If you need to delete and re-create a purchase or transaction with the same external ID:
- Purchases: Update the purchase with
externalId: nullto release the external ID, then create a new purchase with the original external ID. - Transactions: Cancel the transaction with
clearExternalId: trueto release the external ID, then create a new transaction with the original external ID.
Not all loans are current when they migrate. Peach supports migrating loans that are delinquent, accelerated, or charged off.
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.
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.
For large portfolio migrations, follow these operational best practices.
If migrating a large portfolio, segment it into batches. Start small and increase batch size as you gain confidence:
- Start with 50 loans — validate thoroughly
- Increase to 100, then 250, then 500 — monitor for patterns
- 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.
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:
- Migration has succeeded
- Balances have been validated
- Autopay has been configured (if applicable)
- You're ready to sunset the legacy system's communications for that borrower
During migration, both your legacy system and Peach can operate simultaneously. This allows for thorough testing and validation without disrupting your business.
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=trueThis 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):
- Cancel each draw individually, clearing its external IDs.
- Cancel the loan with
clearAllExternalIds=truefor the remaining objects.
After cancellation:
- Create a new loan with the original external ID and repeat the migration process.
- 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.
Peach validates migration data at multiple points. Understanding these rules helps prevent failures.
| Rule | Error symptom |
|---|---|
| No gaps between periods | startDate of period N+1 must equal endDate of period N + 1 day |
| No overlapping start-end date ranges | The startDate–endDate range of one period cannot overlap with another period's range |
| No duplicate dates | Two periods cannot share the same startDate, endDate, statementDate, or dueDate |
| Due date falls in next period | Each period's dueDate must fall between the next period's startDate and endDate |
| Rule | Error symptom |
|---|---|
effectiveTimeOfDay must be after 2:00 AM | Transactions with earlier times fail validation |
External transactions require isExternal: true | Live transactions processed by legacy system must be flagged as external |
| Historical transactions use the past-transaction endpoint | Don't use the standard transaction endpoint for pre-cutoff activity |
| Rule | Error symptom |
|---|---|
| All amounts ≥ 0.00 | Negative amounts are rejected |
| Precision of 0.01 | Amounts with more than 2 decimal places are rejected |
Credit balances use reimbursementAmount | Don't use negative principal for credit balances |
| Overdue balances must match obligations | If 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. |
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:
| Month | Peach (actual days) | 30/360 system | Difference per $10,000 at 18% APR |
|---|---|---|---|
| February (non-leap) | 28 days | 30 days | -$0.99 |
| February (leap) | 29 days | 30 days | -$0.49 |
| March | 31 days | 30 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.
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
effectiveDateto 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.
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.
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.
Q: How long does migration take?
Processing time depends on the number of draws, historical activities (purchases, transactions, fees), and batch size.
| Mode | Behavior | Recommended for |
|---|---|---|
Synchronous (sync=true) | Blocks until migration completes or 60-second timeout | Single loans, testing |
| Asynchronous (default) | Returns immediately, processes in background | Production, 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.
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:
- Rolls back the loan status to
pending - Clears the
migrated_attimestamp - Reverses all ledger entries created during the migration attempt
- Resets the migration status to
failed
You can then reset to prepMigration, correct any data issues, and attempt migration again.
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).
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.
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.
Peach does not fire webhooks during the migration process itself. After migration completes successfully, webhooks begin firing for subsequent loan activity.
| Event | When it fires |
|---|---|
loan.migration.succeeded | Migration completed successfully |
loan.migration.failed | Migration failed and was rolled back |
| Events for live activities | After successful migration, webhooks fire for all events created during replay of live activities |
| Action | Method | Endpoint |
|---|---|---|
| Create borrower | POST | /api/people |
| Create contact | POST | /api/people/{personId}/contacts |
| Create payment instrument | POST | /api/people/{personId}/payment-instruments |
| Create loan (LOC) | POST | /api/people/{personId}/loans |
| List draws | GET | /api/people/{personId}/loans/{loanId}/draws |
| Create draw | POST | /api/people/{personId}/loans/{loanId}/draws |
| Create document | POST | /api/people/{personId}/documents |
| Upload document content | POST | /api/people/{personId}/documents/{docId}/content |
| Create past periods | POST | /api/people/{personId}/loans/{loanId}/migration/past-periods |
| Get past periods | GET | /api/people/{personId}/loans/{loanId}/migration/past-periods |
| Update past period | PUT | /api/people/{personId}/loans/{loanId}/migration/past-periods |
| Delete past period | DELETE | /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 status | PUT | /api/people/{personId}/loans/{loanId} |
| Create purchase | POST | /api/people/{personId}/loans/{loanId}/draws/{drawId}/purchases |
| Create past transaction | POST | /api/people/{personId}/loans/{loanId}/migration/past-transaction |
| Get past transactions | GET | /api/people/{personId}/loans/{loanId}/migration/past-transaction |
| Update past transaction | PUT | /api/people/{personId}/loans/{loanId}/migration/past-transaction/{txnId} |
| Create transaction | POST | /api/people/{personId}/loans/{loanId}/transactions |
| Create fee | POST | /api/people/{personId}/loans/{loanId}/fees |
| Migrate loan | POST | /api/people/{personId}/loans/{loanId}/migrate |
| Get loan (check status) | GET | /api/people/{personId}/loans/{loanId} |
| Get balance | GET | /api/people/{personId}/loans/{loanId}/balance |
| Create autopay | POST | /api/people/{personId}/loans/{loanId}/autopay |
| Cancel fee | PUT | /api/people/{personId}/loans/{loanId}/fees/{feeId}/cancel |
| Reverse transaction | POST | /api/people/{personId}/loans/{loanId}/transactions/{txnId}/reverse |
| Create purchase dispute | POST | /api/people/{personId}/loans/{loanId}/draws/{drawId}/purchases/{purchaseId}/disputes |
| Cancel loan | POST | /api/people/{personId}/loans/{loanId}/cancel |
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.
Every LOC billing cycle is divided into periods defined by four key dates:
| Date | What happens |
|---|---|
| startDate | Period begins. New balance tracking starts. |
| endDate | Period ends. Last day of the billing period. |
| statementDate | Statement generated. Always endDate + 1. Non-due balances move to due. |
| dueDate | Payment deadline. Full balance must be paid by this date for grace (if applicable). |
| dueDate + 1 | Enforcement. 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."
Unlike installment loans where a single payment obligation comes due on one date, LOCs move balances through buckets on specific dates.
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.
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 type | Non-due → due | Time in "due" | Due → overdue |
|---|---|---|---|
| Line of credit | Statement date | 2–3 weeks | Due date + 1 |
| Installment | Due date | 1 day | Due 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."
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_idExpected 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 - fulfilledAmountThe 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.).
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.
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 checkedGrace 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.
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
numPeriodsToRestoreGraceconsecutive periods
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 normallyThe system carries the prior day's grace status forward as a default and updates it on dueDate + 1 (see below).
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 - 1obligations. If all of them were also paid in full, reinstate grace: setisGracePeriodEligible = trueand trigger a replay fromperiod.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 = falseand trigger a replay fromperiod.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.
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.
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.startDateThis 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.
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 - adjustmentAfterStatementAmountFor 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.
Grace can be tracked at two levels:
| Level | Configuration | Behavior |
|---|---|---|
| Loan-level | loc.isGracePeriodApplicable = true | All draws share one grace status. If any draw loses grace, the entire LOC loses grace. |
| Draw-level | draw.atOrigination.gracePeriod.isGracePeriodApplicable = true | Each 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.
- Set
isGracePeriodEligiblecorrectly in the migration period data. If the borrower was in grace before migration, set totrue. If they had lost grace, set tofalse. - Record the grace balance (
gracePeriod.fullBalanceAmountandgracePeriod.fulfilledByDueDateAmount) accurately. These determine whether the due date + 1 check revokes or reinstates grace. - 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. - 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.
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.
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.
Peach supports four draw types:
| Draw type | Purpose | Typical use | Interest behavior |
|---|---|---|---|
| RegularPurchase | Standard retail purchases | Primary draw type for card activity | Standard APR |
| CashAdvance | Cash withdrawal or equivalent | ATM withdrawals, cash-like transactions | Often higher APR; may have different fees |
| BalanceTransfer | Transferred balance from another card | Promotional offers | Often 0% APR promotional rate |
| Static | Migration draw (historical data) | Created automatically at migration | No interest accrual; no new activity after migration |
Most active LOCs have multiple draws. The Static draw is special and used only during migration.
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.
Fees can be charged at draw creation or throughout the draw's lifecycle:
| Fee type | When charged | Example |
|---|---|---|
| Draw fee | At draw creation | $75 cash advance origination fee |
| Transaction fee | Per transaction | 3% foreign transaction fee |
| Periodic fee | Each billing period | $5/month servicing fee |
| Late fee | When draw goes overdue | Automatic, 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.
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.).
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:
| Property | Value | Why |
|---|---|---|
drawType | Static | Identifies it as the migration draw |
timestamps | Equal to LOC activation date | Appears to have existed since LOC origination |
min_payment_percentage_of_principal | 0 | Static draw doesn't contribute to minimum payment |
migration_status | Synced with LOC | Follows 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.
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.
- The static draw is created automatically — don't create it manually.
- Post all historical purchases to the static draw using
originalDrawIdto reference the actual legacy draw. - Create actual draws (RegularPurchase, CashAdvance, etc.) for any draw types that were active before migration and should continue to accept new activity.
- Populate migration period data for each draw — the system validates that every non-static draw has a ledger update event.
- After migration, all new activity goes to actual draws. The static draw is read-only.
- Draw fees must be configured correctly on each draw's
atOrigination.feesobject, and fee balances must be seeded in the migration period draw data.