# LOC migration procedure

## Overview

This runbook walks through migrating a single line-of-credit account into Peach with its full history — the borrower and payment methods, the loan and its draws, historical billing periods, the balance snapshot at the migration cutoff date, historical and live activity, and the final migrate call that validates the data and activates the loan. It is written for integration engineers executing a migration end to end; each step shows the API call that drives it and how to confirm the call succeeded.

This page covers the procedure through a successful migrate call. It does **not** cover:

- Post-migration validation, autopay, credit reporting, and ongoing system behavior — see [After LOC migration](/loan-lifecycle/loc-migration/after-migration).
- Correcting, disputing, or reversing migrated data — see [Modify data after LOC migration](/loan-lifecycle/loc-migration/modifying-data-after-migration).
- Fees, statement scheduling, and delinquent, accelerated, or charged-off states — see [LOC migration special cases and configuration](/loan-lifecycle/loc-migration/special-cases-and-configuration).
- Batching, sequencing, and re-migration for large portfolios — see [LOC migration strategy and operations](/loan-lifecycle/loc-migration/strategy-and-operations).


The diagram below shows the full call sequence across the five steps. Each call is detailed in the step it belongs to.


```mermaid
sequenceDiagram
    participant I as Integrator
    participant P as Peach API

    Note over I,P: Step 1 — Borrower and payment methods
    I->>P: POST /people
    P-->>I: personId
    I->>P: POST /people/{personId}/contacts
    I->>P: POST /people/{personId}/payment-instruments

    Note over I,P: Step 2 — Loan and draws
    I->>P: POST /loans (status pending, migrationStatus prepMigration)
    Note right of P: Peach auto-creates the static migration draw
    P-->>I: loanId
    I->>P: GET /loans/{loanId}/draws
    P-->>I: migration draw id (drawType static)
    I->>P: POST /loans/{loanId}/draws (active draws)
    I->>P: POST /documents then POST /documents/{id}/content

    Note over I,P: Step 3 — Historical data
    I->>P: POST /migration/past-periods
    I->>P: POST /migration/period
    I->>P: POST /draws/{drawId}/migration/period (per active draw)
    I->>P: PUT /loans/{loanId} (status originated)

    Note over I,P: Step 4 — Activities
    I->>P: POST purchases, past-transactions, transactions, fees

    Note over I,P: Step 5 — Execute
    I->>P: POST /loans/{loanId}/migrate
    Note right of P: migrationStatus prepMigration to migrating
    P-->>I: 204, sync waits for completion and async returns at once
    I->>P: GET /loans/{loanId} (poll migrationStatus)
    P-->>I: migrationStatus completed, loan active
    Note right of P: Fires loan.migration.succeeded
```

## Assumed knowledge

- [How LOC billing cycles work](/loan-lifecycle/loc-migration/how-loc-billing-cycles-work) — required to understand the balance breakdowns in Step 3.
- [How grace periods work for lines of credit](/loan-lifecycle/loc-migration/how-grace-periods-work) — grace eligibility seeded in the migration period data.
- [How LOC draws work](/loan-lifecycle/loc-migration/how-loc-draws-work) — the migration draw and the active draws used throughout.


## 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.


```json
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):


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

**Expected result:** The response returns the new borrower's `personId` (for example, `BO-1234-ABCD`) with `status: "active"`. Save the `personId` — every later call in this procedure uses it.

### Create contact records

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


```json
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.).

**Expected result:** Each call returns the created contact record with its own contact ID. Repeat until every address, email, and phone number from the legacy record exists in Peach.

### 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):


```json
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.


```json
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`.

**Expected result:** Each call returns the created payment instrument with a `PI-...` id. Save the IDs you'll reference on transactions later in the procedure.

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.


```json
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):


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

**Expected result:** The response returns the loan with `status: "pending"` and `migrationStatus: "prepMigration"`. At this point Peach automatically creates the static migration draw, which you retrieve in the next step.

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](/loan-lifecycle/loc-migration/after-migration#re-enable-borrower-communications) 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) |


### 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.


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

**Response** (abbreviated):


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

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

**Expected result:** The draws list includes the auto-created migration draw with `drawType: "static"` and `status: "pending"`. If no static draw is present, confirm the loan was created with `migrationStatus: "prepMigration"`.

### 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.


```json
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):


```json
{
  "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.

**Expected result:** Each call returns the created draw with `status: "pending"` and its `id`. Create one for every draw that should remain live after the cutoff date.

### Upload loan documents

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


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

{
  "type": "loanAgreement",
  "description": "Original LOC Agreement",
  "status": "accepted",
  "loanId": "LN-1234-ABCD",
  "sensitiveData": false
}
```


```http
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.

**Expected result:** The descriptor call returns a `documentDescriptorId`; the content call attaches the file to it. Save each `documentDescriptorId` you intend to reference on a past period.

## 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`.
- A period's `dueDate` must not be after the following period's `endDate`. (Peach enforces only this upper bound; there is no lower-bound check against the following period's `startDate`.)
- No two periods can have overlapping start-end date ranges.



```json
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 |


**Expected result:** A `201` response echoing the created period snapshots. These appear in the borrower's statement history but are not replayed during migration.

### 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.


```json
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`. |


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

**Expected result:** The response stores the line-level balance snapshot and obligation for the migration period. This snapshot becomes the line-level ledger update event that the migrate call validates.

### Create migration period draw data

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


```json
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.

**Expected result:** The response stores each draw's balance snapshot and obligation. Every non-static draw must have one — otherwise the migrate call fails with `Loan is missing ledger update event`.

### Change loan status to Originated

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


```json
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.

**Expected result:** The loan returns with `status: "originated"`. The `atOrigination` object is now locked and cannot be changed.

## 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.


```json
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`.

**Expected result:** The response returns the created purchase attached to the migration draw, carrying the `migration.originalDrawId` you supplied. Confirm it has a terminal status (`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.


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

**Expected result:** The response returns the created purchase attached to the actual draw (not the migration draw), with no `migration` object.

### 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.


```json
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:


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

**Expected result:** The response returns the created past transaction with its `drawSplitDetails`. Confirm the split references only active draw IDs — these are required for any post-migration status change or reversal.

### 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.


```json
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.
- There is no minimum-hour rule on `effectiveTimeOfDay` — transactions are accepted at any time of day. For migration purchases specifically, Peach internally stamps the timestamp at 6:00 AM in the product timezone so it stays clear of the daily loan maintenance (DLM) "deadzone" (the ~1:00 AM window in the product timezone around which interest accrual dates are determined). You do not need to adjust the times you submit.


**Expected result:** The response returns the created transaction flagged `isExternal: true`, applied as of its `effectiveDate`.

### 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`.


```json
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 |


**Expected result:** The response returns the created fee associated with the correct legacy draw, transaction, or purchase via the matching `migration.*` field.

### Live fees (on or after the migration cutoff date)

Create live fees normally, without the `migration` object.


```json
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
  }
}
```

**Expected result:** The response returns the created fee with no `migration` object, charged to the loan as of its `chargeDate`.

## 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


```json
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: 20 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.** |


**Expected result:** A `204` response ("Loan migration started"). In synchronous mode the `204` returns only once migration finishes; if it runs past the 20-second timeout you receive `408 Request Timeout` and migration continues in the background. In asynchronous mode the `204` returns immediately and you poll for status.

### Monitor migration status

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


```json
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 |


The diagram below shows the full `migrationStatus` lifecycle, including the initial `nonMigrated` state (before you set `prepMigration` on loan creation) and the retry path out of `failed`. A `completed` migration is terminal — you cannot move it back to `prepMigration`.


```mermaid
stateDiagram-v2
    [*] --> nonMigrated
    nonMigrated --> prepMigration: create loan with migrationStatus prepMigration
    prepMigration --> migrating: POST /migrate
    migrating --> completed: validation and replay succeed, loan becomes active
    migrating --> failed: validation or replay fails, ledger rolled back, loan reverts to pending
    failed --> prepMigration: reset to prepMigration and retry after corrections
    completed --> [*]
```

**Expected result:** `GET` returns the current `migrationStatus`. A successful migration advances it to `completed` (loan becomes `active`); a failure sets it to `failed` (loan reverts to `pending`).

### 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 `pending` → `originated` → `active`.
- `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`:



```json
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

- [After LOC migration](/loan-lifecycle/loc-migration/after-migration) — Validate balances, configure autopay and credit reporting, and post-migration behavior.
- [LOC migration validation and troubleshooting](/loan-lifecycle/loc-migration/validation-and-troubleshooting) — Validation rules and a troubleshooting FAQ.
- [LOC migration API and webhooks reference](/loan-lifecycle/loc-migration/api-and-webhooks-reference) — The endpoints used across this procedure.