Loan stages and value
Event: a loan starts, moves through its stages, and funds (or dies).
Endpoint: POST /v1/conversion with stage
Status: live in dev, verified end to end
Who sends this. Stitch does not know and does not need to. It sees a key, which names the system the owner issued it to, and a payload. Today the likeliest emitter is the loan origination system, or a middleware in front of it. Stitch makes no assumption about which stages that system has, in what order it reaches them, or when it reports them: send the stages you have, when you have them.
The loan is the conversion object. Everything Stitch reports about performance ultimately resolves to a loan and its value, so this is the event that makes every report non-empty.
Authentication
Section titled “Authentication”Authorization: Bearer <your key>One key per emitting system, named by the owner after whatever actually sends;
401 {"error":"unauthorized"} without it, and nothing written. The system a
key belongs to is recorded on every row it creates as source_system.
1. A loan is created only at application start
Section titled “1. A loan is created only at application start”Nothing before app_started creates a loan. Pre-application milestones belong
to the person and the lead (lead-events.md). This keeps
three clean grains with unambiguous denominators.
curl -X POST https://stitch-collector-dev.magic-blocks.workers.dev/v1/conversion \ -H "content-type: application/json" \ -H "authorization: Bearer <your key>" \ -d '{ "idempotency_key": "loan-L88213-app_started", "identity": { "relcu_contact_id": "88213" }, "loan": { "external_id": "L-88213", "lead_external_id": "44821", "brand_id": "bll", "attrs": { "product": "dscr", "app_type": "purchase", "occupancy": "investment", "property_status": "tbd", "loan_amount": 412000, "property_value": 515000, "credit_range": "Above 740" } }, "stage": "app_started", "occurred_at": "2026-03-03T09:12:00Z" }'lead_external_id is the single most important field here
Section titled “lead_external_id is the single most important field here”When the loan carries the id of the lead it came from, the lead → loan link is deterministic — no inference, no matching heuristics. That gives every loan a clean, non-overlapping attribution window inherited from its lead.
Send it on the first loan event. Without it the loan is an orphan application: it still lands, it is still counted, and it appears on the data quality report as a monitor — but it cannot be attributed, because there is no anchor to run a window back from. Stitch will never guess one. A rising orphan rate means applications are starting outside the tracked lead flow — manual entry, a broken integration — and is worth an alert.
The value model’s inputs
Section titled “The value model’s inputs”Stitch predicts what each application will earn from five fields, as the
applicant entered them: loan_amount and property_value in dollars (or
loan_amount_cents / property_value_cents), the self-reported credit range
as credit_range or credit_band (either generation of the form’s options,
e.g. "Above 740", "Between 700 To 719"), app_type and product. Send
them on the first application event you have them for, and send fico on
whichever event first carries the pulled score; Stitch re-scores when it
arrives. Property value matters most: a round figure and an exactly-80%
loan-to-value are the model’s strongest signals, so send the number as typed,
never a value derived from a ratio. An application with a product the model
does not cover (a HELOC) is stored and counted but given no value.
Stitch scores a loan once, at the first event that finds it at
app_submitted or beyond with these inputs present, and sends the
application conversion then. It does not matter whether the pulled score
arrives on the same event or a later one; the re-score handles the second
case. See application-value.md for what moves the
value afterwards and what does not.
2. Stage transitions
Section titled “2. Stage transitions”One call per transition. Same shape, less payload.
curl -X POST https://stitch-collector-dev.magic-blocks.workers.dev/v1/conversion \ -H "content-type: application/json" \ -H "authorization: Bearer <your key>" \ -d '{ "idempotency_key": "loan-L88213-funded", "identity": { "relcu_contact_id": "88213" }, "loan": { "external_id": "L-88213" }, "stage": "funded", "value_cents": 988000, "occurred_at": "2026-04-14T16:40:00Z" }'Stages Stitch recognises: app_started, app_submitted, credit_pulled,
conditional_approval, appraisal, docs_out, clear_to_close, funded,
and the terminal withdrawn, denied, dead. The list is Stitch’s
vocabulary, not a claim about any system’s workflow: map your stages onto it,
send the ones you have, in whatever order they really happen. Nothing in
Stitch depends on receiving all of them or receiving them in sequence.
funded carries the realised value and fires the funded conversion to the
ad platforms; the dead ends restate the application’s value to the floor
(see outbound.md).
3. Attributes change over time, and that is recorded, not overwritten
Section titled “3. Attributes change over time, and that is recorded, not overwritten”Every loan attribute is versioned with a validity window. Send an attribute
again with a new occurred_at and Stitch keeps both, with the transition
timestamped.
curl -X POST https://stitch-collector-dev.magic-blocks.workers.dev/v1/conversion \ -H "content-type: application/json" \ -H "authorization: Bearer <your key>" \ -d '{ "idempotency_key": "loan-L88213-appraisal", "identity": { "relcu_contact_id": "88213" }, "loan": { "external_id": "L-88213", "attrs": { "property_status": "identified", "property_state": "AZ", "property_type": "multi_family" } }, "stage": "appraisal", "occurred_at": "2026-03-20T10:00:00Z" }'This is what makes “TBD → identified property” a measurable conversion in its own right, with its own lag distribution by channel. A TBD lead that becomes a Phoenix duplex counts as TBD in acquisition reporting and as Phoenix multi-family in portfolio reporting. Both are correct. Most tools force you to pick one and are silently wrong in the other.
So: do not wait for attributes to settle. Send what you know when you know it, and send the correction later.
Recognised attributes: product, app_type, occupancy,
property_status, property_state, property_type, loan_amount,
property_value, ltv, credit_range / credit_band, fico,
channel_of_origin, expected_revenue, actual_revenue, lo_assigned.
Unrecognised keys are stored too, so a new attribute needs no code change
here.
The amount the loan ends with
Section titled “The amount the loan ends with”If the figures on the file change after the application, send the current
loan_amount / property_value in loan.attrs on later stage events
whenever they change. Stitch never re-scores on them — the value model reads
the borrower’s own entry — but records them, and the predictions export
reports them beside the scored inputs as final_loan_amount,
final_property_value and loan_amount_change, which is how the refit
measures the edit rate.
4. Value
Section titled “4. Value”Three measures, each with a realisation state.
value_measure | value_state | When |
|---|---|---|
gross_revenue | realized | At funding. The truth, arriving late |
predicted_ev | predicted | From app start, revised over time |
contribution_margin | realized | At funding, if finance supplies it |
Predicted EV is Stitch’s; yours is optional
Section titled “Predicted EV is Stitch’s; yours is optional”Stitch computes predicted_ev itself from the inputs above and writes a
revision with its model version each time one changes. You may still send
your own predicted_ev; it is stored as an external revision and used only
for a loan Stitch could not score.
If you do send it, send every revision, not just the latest
Section titled “If you do send it, send every revision, not just the latest”curl -X POST https://stitch-collector-dev.magic-blocks.workers.dev/v1/conversion \ -H "content-type: application/json" \ -H "authorization: Bearer <your key>" \ -d '{ "idempotency_key": "loan-L88213-ev-r3", "identity": { "relcu_contact_id": "88213" }, "loan": { "external_id": "L-88213" }, "stage": "app_submitted", "value_cents": 620000, "value_measure": "predicted_ev", "value_state": "predicted", "occurred_at": "2026-03-10T08:00:00Z" }'Stitch keeps every revision. That gives a report almost nobody has: predicted
EV versus realised revenue, by channel, by product, by prediction age. If
leads from one channel systematically over-predict, that is a scoring-model
bias that silently misroutes budget — and it is invisible unless the revisions
are retained. Use a distinct idempotency_key per revision, or later
revisions are treated as duplicates and dropped.
Contribution margin is optional and must stay optional
Section titled “Contribution margin is optional and must stay optional”Nothing in Stitch requires it. Every report degrades to gross_revenue when
margin is absent and says so, rather than showing a hole or a misleading zero.
5. Idempotency
Section titled “5. Idempotency”idempotency_key must be unique per event and stable across retries — and
unique across every event and every emitter, lead or loan, because Stitch
keeps one table of them. Prefix it with the emitter’s name:
{emitter}-loan-{loan_number}-{stage}, e.g. los-loan-L88213-funded. Re-sending returns 200 with
"duplicate": true and changes nothing. Fire the same message a hundred
times if your retry logic wants to.
6. Open question for whoever owns this event
Section titled “6. Open question for whoever owns this event”Which of the stages above exist in your workflow, and does the borrower’s
credit pull happen before or after the application reaches you? Stitch works
either way; the answer only changes which event carries fico and how the
docs describe the handoff.
7. Smoke test
Section titled “7. Smoke test”STAMP=$(date +%s)curl -sS -X POST https://stitch-collector-dev.magic-blocks.workers.dev/v1/conversion \ -H "content-type: application/json" \ -H "authorization: Bearer <your key>" \ -d "{\"idempotency_key\":\"smoke-$STAMP-funded\",\"identity\":{\"relcu_contact_id\":\"smoke-$STAMP\"},\"loan\":{\"external_id\":\"smoke-L-$STAMP\"},\"stage\":\"funded\",\"value_cents\":988000,\"occurred_at\":$(date +%s)000}"Expect 202 (a 401 means the key is missing or wrong) and a loanId.
Re-send and expect 200 with "duplicate": true.