Help CenterPayment History Grid
    Records8 min read

    Payment History Grid

    A color-coded 24-cell visualization of every account's Payment History Profile. Spot bad PHP strings, explain delinquency patterns, and audit the codes you're sending to the bureaus - all without parsing a 24-character string by hand.

    1. What it is

    The Payment History Grid is a 24-cell strip that visualizes the Metro 2 Payment History Profile (PHP) - Field 18 in the CDIA Credit Reporting Resource Guide. Each cell represents one month, and each color/letter encodes the consumer's payment status for that month.

    PHP is the field bureaus look at to tell, at a glance, whether a tradeline has been clean, late, or already worked. Metro2 stores PHP as a single 24-character string per record:

    payment_history_profile: "0000000011220000000000BB"

    That string is hard to read and even harder to QA at scale. The grid turns it into something you can scan in under a second:

    The grid is the single source of truth for PHP in the UI.
    Anywhere you see PHP in Metro2 - the record form, the records table, the file-upload analytics page - it's rendered by the same Metro2PHPGrid component, fed by the same code legend, with the same color palette. There is no second renderer that could disagree.

    2. Why it matters

    The bureaus interpret PHP literally. A single wrong character is reported as fact to Experian, Equifax, and TransUnion, and from there it flows into every credit score that consumes your data.

    • A leading L (charge-off) when the account is actually current can knock 100+ points off a consumer's score and trigger an FCRA dispute that you have to investigate within 30 days.
    • A 24-string padded with B (no history) when the account actually has 18 months of clean payments tells the bureaus you haven't been furnishing - which can fail bureau onboarding QA.
    • A code outside the CRRG-allowed alphabet (anything other than 0-6, B, D, E, G, H, J, K, L) will reject the entire base segment during bureau validation.

    The grid exists so that bad PHP gets caught by a human eye before it leaves your system, not by a bureau rejection report two weeks later.

    3. Reading the grid

    Position 1 (most recent month) is on the left; position 24 (oldest month) is on the right. This matches the left-to-right order of the underlying PHP string and how the validator and generator read it.

    Some CDIA training diagrams put month 1 on the right.
    The data layout in the spec is unambiguous - left-most character of the PHP string is always the most recent month. Metro2 renders in the same direction as the data, so what you see on screen matches what gets written to the file byte-for-byte.

    The full code legend

    There are 15 allowed CRRG codes. Metro2 uses an Okabe-Ito color-blind-safe palette so that red/green confusion never hides a delinquency.

    CodeColorMeaningSeverity
    0Current / paid on time1 (good)
    130 days past due2 (mild delinquency)
    260 days past due3 (moderate)
    390 days past due3 (moderate)
    4120 days past due4 (severe)
    5150 days past due4 (severe)
    6180+ days past due4 (severe)
    BNo history available (account didn't exist that month)0 (neutral)
    DNo history reported - account paid or closed0 (neutral)
    EZero balance and current account (informational)6 (dispute / info)
    GCollection5 (terminal negative)
    HForeclosure completed5 (terminal negative)
    JVoluntary surrender5 (terminal negative)
    KRepossession5 (terminal negative)
    LCharge-off5 (terminal negative, most severe)

    How to read a real example

    Suppose a record's PHP looks like this:

    "00000112200000000BBBBBBB"

    Reading left to right (most recent month first):

    • Months 1-5: green (0) - account is current and has been for the last 5 months.
    • Months 6-7: yellow (1) - 30 days past due. The consumer was a month late twice.
    • Months 8-9: orange (2) - 60 days past due. The delinquency deepened before they caught up.
    • Months 10-17: green (0) - back to current.
    • Months 18-24: gray (B) - account didn't exist yet, so there's no history to report.

    That tells a coherent story: the account opened ~17 months ago, had a rough patch in months 6-9, and has been clean since. If the grid shows a jumble that doesn't tell a story like that - e.g. a stray charge-off (L) in the middle of green cells - that's a data-quality red flag.

    Tooltips

    Hover any cell to see its position number, the calendar month it represents, the code character, and the plain-English meaning. The month is derived from date_of_account_info on the record, so it always matches what gets reported to the bureau.

    4. Where you'll see it

    Per-record view (Records - Edit Record)

    Open any record from the Records table and scroll to the Payment History Profile section in the form. You'll see a medium-size grid with a live preview that re-renders whenever you change one of the five driver fields (see section 6). The grid is read-only here; the PHP string itself is derived on save, not typed.

    Per-record drill-down (Records table)

    Expand any row in the records manager and you get a compact "sm" grid inline - useful for scanning a hundred accounts at once without opening each one. The "worst" code for each account is also shown as a colored chip so you can sort or filter by severity.

    Per-file portfolio heatmap (Analytics)

    From the Analytics tab (or the file-upload detail page), the Payment History Profile - Portfolio tile renders a 24-by-15 heatmap: 24 position columns (most recent on the left) by 15 code rows. Each cell shows what fraction of accounts in scope carry that code at that position. Hover any cell for the exact count and percentage.

    Pass a fileUploadId to scope the heatmap to a single upload; omit it to see the entire company's portfolio. The tile defaults to portfolio-wide when no file is selected.

    5. What "derived" vs "stored" means

    Metro2 keeps two views of every PHP value on every record:

    ViewWhat it isWhere it comes from
    StoredThe 24-char string currently saved on the record and the value that will be written into the next generated Metro 2 file.metro2_records.payment_history_profile
    DerivedWhat PHP would be if we recomputed it from the driver fields right now. Used as a sanity-check overlay.computePaymentHistoryProfileFromRecord()

    In Wave 2 / Phase 1 (where the product is today), stored and derived are kept in lockstep: the record handler re-derives PHP from the driver fields on every save and overwrites whatever was in the stored column. That's why effective = stored for now - there is no scenario where a manual edit causes the two to diverge.

    Cells that disagree get a yellow ring.
    When the grid is rendered with both value (stored) and a derived prop, any cell where the two differ is outlined in amber. In practice you should never see this in normal use; if you do, it's a strong signal the record was edited outside the normal save path (e.g. a backfill migration) and should be re-saved or investigated.

    6. Driver fields

    Metro2 derives PHP from five fields on the record. Change any of these and the grid in the record form updates live:

    FieldWhat it tells the derivation
    date_openedAnchors the start of history. Months before this date are rendered as B (no history available) because the account didn't exist yet.
    account_statusDrives the code for position 1 (and carry-forward for terminal states like charge-off and collection - see common questions below).
    date_of_account_infoThe "as-of" date for the entire PHP string. Position 1 is the month before this date; position 24 is 24 months before.
    fcra_date_of_first_delinquencyThe earliest month a delinquency can appear. Combined with account_status, used to back-fill the months between DOFD and the as-of date.
    date_closedMarks where the account stops reporting active history. After this date, cells become D (paid/closed) or follow a terminal carry-forward.

    Because PHP is strictly derived, the way to "edit" the grid is to edit these five fields. There is no PHP textbox; the string you see is always a function of those inputs.

    7. Worst-code summary

    Every record has a worst code - the single most severe code in its 24-month string, by severity bucket (0 = no history, 1 = current, ..., 5 = terminal negative). The grid surfaces this on the per-record view and the portfolio heatmap uses it to build a histogram across all accounts.

    The portfolio heatmap displays worst-code distribution as a row of badges, sorted by severity:

    Worst-code per account (severity)
    
    L · Charge-off                              42
    G · Collection                              17
    3 · 90 days past due                         8
    1 · 30 days past due                       115
    0 · Current                              4,238

    Two things to look at first: the count of accounts whose worst code is L, G, H, J, or K (anything terminal-negative), and any unexpected clustering. A sudden spike in L for a file you didn't expect to charge-off anything in is usually a mapping bug in the upstream system, not a real wave of charge-offs.

    What counts as 'worst'?
    Severity is a bucket, not a score. Codes 1 through 6 get progressively worse, then collection (G), foreclosure (H), surrender (J), and repossession (K) are all severity 5, with charge-off (L) being the most severe terminal state. The "no history" codes (B, D) are severity 0 and never count as worst - a brand-new account doesn't get flagged just because most of its cells are gray.

    8. Editing PHP

    PHP is read-only in v1.
    You cannot manually overwrite a single cell of the grid today. PHP is derived from the five driver fields on every save, and the API intentionally rejects any user-supplied PHP value.

    Why? Two reasons:

    • Compliance. Manually overriding PHP is a regulated act under the FCRA. If an override is used to mask a delinquency (intentionally or not), the furnisher carries the liability. We want overrides to require a mandatory reason, an audit log entry, and ideally a legal sign-off on the policy - none of which exists in v1.
    • Determinism. While PHP is strictly derived, you have a single source of truth: change the driver fields, get a new PHP. As soon as overrides exist, every screen that displays PHP has to distinguish "what the model says" from "what a human overrode", and every bureau dispute investigation has to chase both.

    A roles-gated, audit-logged inline override workflow is on the roadmap (Phase 3 of the payment-history-grid plan), but is not scheduled until the compliance posture is signed off. Until then: edit the driver fields and let the derivation do the work.

    If you have a legitimate need to correct PHP that derivation can't express (typically: a portfolio migration where the historical ledger doesn't fit Metro2's derivation model), contact support and we'll work through the records with you directly.

    9. API reference

    Both grid endpoints are read-only GET requests. Use a session cookie (from a logged-in browser) or an API key in the Authorization: Bearer header.

    GET /api/metro2-record-php

    Returns the stored, derived, and effective PHP for a single record, plus the driver fields it was derived from and the worst-code summary.

    curl -s "https://metro2.switchlabs.dev/api/metro2-record-php?id=<RECORD_ID>" \
      -H "Authorization: Bearer <API_KEY>"

    Response shape:

    {
      "success": true,
      "data": {
        "recordId": "9c7e...",
        "accountNumber": "ACC-00042",
        "stored":    "00000112200000000BBBBBBB",
        "derived":   "00000112200000000BBBBBBB",
        "effective": "00000112200000000BBBBBBB",
        "overrides": [],
        "driverFields": {
          "date_opened":                    "2024-12-01",
          "account_status":                 "11",
          "date_of_account_info":           "2026-05-31",
          "fcra_date_of_first_delinquency": "2025-09-01",
          "date_closed":                    null
        },
        "worstCode": {
          "code": "2",
          "meaning": "60 days past due",
          "severity": 3
        }
      }
    }

    The overrides array is reserved for Phase 3 and is always empty today. effective equals stored for the same reason.

    GET /api/metro2-analytics/php-distribution

    Returns the 24-by-15 aggregate matrix used by the portfolio heatmap, plus the worst-by-record histogram.

    curl -s "https://metro2.switchlabs.dev/api/metro2-analytics/php-distribution?\
    companyId=<COMPANY_ID>&fileUploadId=<FILE_UPLOAD_ID>" \
      -H "Authorization: Bearer <API_KEY>"

    Response shape (trimmed):

    {
      "success": true,
      "data": {
        "scope": {
          "companyId":    "fa90...",
          "fileUploadId": "8b1c..."
        },
        "totalRecords": 4420,
        "totalCells":   106080,
        "counts": [
          { "position": 1,  "byCode": { "0": 4238, "1": 115, "2": 8, "3": 0, ... } },
          { "position": 2,  "byCode": { "0": 4231, "1": 122, "2": 9, "3": 0, ... } },
          ...
          { "position": 24, "byCode": { "0": 2150, "1": 38,  "B": 2200,        ... } }
        ],
        "worstByRecord": {
          "0": 4238, "1": 115, "2": 8, "3": 0, "4": 0, "5": 0, "6": 0,
          "B": 0,   "D": 0,   "E": 0, "G": 17, "H": 0, "J": 0, "K": 0, "L": 42
        }
      }
    }
    • companyId is optional - defaults to the caller's own company. Superadmins can pass another company.
    • fileUploadId is optional - omit to get a portfolio-wide view across every record in the company.
    • Every code is seeded with zero in byCode so consumers can render a complete heatmap even for sparse buckets.

    10. Common questions

    Why does my grid show a code I didn't expect at position 1?

    Position 1 is derived from the account's current account_status. If that's a terminal status (charge-off, collection, repo, foreclosure, surrender), the terminal code carries forward into position 1 even if no payment activity occurred that month. That's CRRG-compliant behavior - once an account is charged off it stays charged off until reported as paid.

    If you're seeing a 1 when you expected 0, check fcra_date_of_first_delinquency - if it's set to the current month, the derivation will mark the most recent cell as the start of a delinquency.

    Why are most of my cells gray?

    Gray (B) means "no history available". The most common cause is an account that's less than 24 months old - everything before date_opened is gray because the account didn't exist yet. If you opened the account 6 months ago, you'll see 6 colored cells on the left and 18 gray cells on the right.

    Lighter gray (D) means "no history reported - account paid or closed". That's the right code for months after date_closed.

    Why is a single cell yellow when everything around it is green?

    Most likely you have a single late-payment event in your driver data - typically reflected in fcra_date_of_first_delinquency - that the derivation is correctly translating into a 30-day-late code (1) for that month. If that's not what actually happened, audit the source data; the grid is showing you exactly what your record says.

    Can I export the grid?

    Not as an image today. You can hit the JSON endpoints above and render however you like, and PDF export is on the Phase 4 roadmap (will hook into the existing analytics-suite PDF pipeline). For now, screenshot is the fastest path.

    Does the grid show what gets sent to the bureaus?

    Yes - exactly. The grid is rendered from the same payment_history_profile string that gets written into Field 18 of the base segment of your generated Metro 2 file. There's no transformation in between.

    Canadian (CN) jurisdiction - does the grid work the same?

    Yes. The CRRG alphabet for PHP is identical between US and Canada, so the same 15 codes, the same colors, the same derivation rules all apply.

    11. Troubleshooting

    Stored and derived don't match

    If you load a record and the grid has amber-ringed cells (meaning stored differs from derived), the most likely causes are:

    • The record was bulk-imported or backfilled outside the normal save path, which skipped the re-derivation step.
    • A driver field was edited directly in the database (via a SQL script, for example) without going through the record save handler.
    • The record was created before a derivation rule change and hasn't been re-saved since.

    Fix: open the record in the form and save it (no need to change anything). The save handler re-derives PHP and writes the result back. If you have a large backfill to fix, use the bulk update API with an empty patch body to trigger re-derivation across many records at once.

    Some cells appear to be missing

    The grid always renders exactly 24 cells. If you only see, say, 6 colored cells and the rest are gray, that's not "missing" - that's the grid correctly showing that you only have 6 months of history (account opened < 24 months ago) and the other 18 months are B.

    The portfolio heatmap is empty

    The heatmap will show an empty state if the scope filter returns zero records. Check:

    • The fileUploadId parameter is valid and matches a file that has imported records (not a failed/empty upload).
    • The company actually has records (visit the Records tab and confirm).
    • You haven't filtered to a date window with no records. The heatmap is unfiltered by date in v1 - it runs across every record in scope.

    The tooltip shows a different month than I expect

    Tooltip month labels are computed as date_of_account_info - position. Position 1 is the month before the as-of date, not the same month. So a record with date_of_account_info = 2026-05-31 will label position 1 as April 2026, position 2 as March 2026, etc. This matches how the validator and the generator interpret the field.

    The grid renders blank or shows ? cells

    A ? means the stored string contains a character not in the CRRG alphabet. This should never happen for records that pass validation; if you see one, the record was created outside the validator path. Re-save the record to trigger re-derivation and the bad character will be replaced.

    • AI Error Explanations - turn cryptic PHP validation codes into plain-English explanations and suggested fixes.
    • Analytics & Reporting Suite - the dashboard that hosts the portfolio PHP heatmap alongside DQ metrics, audit reports, and submission reconciliation.
    • Help Center - browse every Metro2 guide.

    Still stuck? Contact support or browse the Help Center.