Skip to content

LOC migration procedure

Assumed knowledge

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.

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"
  }
}

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.

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"
}
Irreversible after origination

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"
  }
}

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

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
  }
}
  • 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.

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

See also