{"openapi":"3.1.0","info":{"title":"POST /api/v1/credit/programs/{program_id}/loans/import","version":"1.0.0","description":"Dry-run or apply a staged loan file across the program"},"servers":[{"url":"https://api.ondayzero.com","description":"Production"}],"paths":{"/api/v1/credit/programs/{program_id}/loans/import":{"post":{"tags":["credit"],"summary":"Dry-run or apply a staged loan file across the program","description":"Fans the staged rows out to each line's loan import (1,000 rows per call) under one savepoint. With `dry_run` (the default), or any control total that does not match across the whole file, nothing is written, not one line; otherwise every line is written together. `create_missing_lines` opens a line for each key the program lacks, with `line_defaults`, through the ordinary line opening (so the program's limit envelope applies); a refusal rejects that line's rows. `mode` says where it runs: in the request (`201` with the per-line results, the totals against the control totals, and the report id), or as a background job (`202` with the batch, `status` `running`; poll `GET /credit/import-batches/{id}` for its progress and, when it finishes, the same result). `auto`, the default, runs up to 1,000 rows in the request. One run at a time per program. Operator action: an API token receives `403`.","operationId":"credit_import_run_loans","parameters":[{"name":"program_id","in":"path","required":true,"schema":{"type":"string","title":"Program Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-business-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Business-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProgramLoanImportRequest"}}}},"responses":{"201":{"description":"Run in the request: the per-line results, the totals against the control totals, and the report id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope_ProgramLoanImportResponse_"}}}},"202":{"description":"Queued as a background job: the batch, `status` `running`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope_ImportBatchResponse_"}}}},"400":{"description":"Bad Request - Invalid input","content":{"application/json":{"example":{"error":"validation_error","message":"Validation failed: due_date: Input should be a valid date","errors":{"due_date":"Input should be a valid date"},"code":"GEN_002","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized - Missing or expired token, an MCP OAuth token used against the REST API, or no access to this business","content":{"application/json":{"example":{"error":"unauthorized","message":"Authentication required","code":"AUTH_001","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden - Insufficient permissions","content":{"application/json":{"example":{"error":"forbidden","message":"You don't have permission to access this resource","code":"AUTH_002","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found - Resource does not exist","content":{"application/json":{"example":{"error":"not_found","message":"Resource not found","code":"NOT_FOUND_001","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"validation_error","message":"Validation failed: amount: Input should be a valid integer","errors":{"amount":"Input should be a valid integer"},"code":"GEN_002","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"}}}}},"x-fastapi-operation-id":"credit_import_run_loans_api_v1_credit_programs__program_id__loans_import_post"}}},"components":{"schemas":{"ImportBatchResponse":{"properties":{"staging_id":{"type":"string","title":"Staging Id","description":"The batch id. Also the report id."},"program_id":{"type":"string","title":"Program Id","description":"The program the file is for."},"filename":{"type":"string","title":"Filename","description":"The file's name as uploaded."},"sha256":{"type":"string","title":"Sha256","description":"SHA-256 of the file as uploaded."},"row_count":{"type":"integer","title":"Row Count","description":"Rows in the file, header excluded."},"status":{"type":"string","enum":["parsed","running","dry_run","applied","failed","expired"],"title":"Status","description":"parsed, running (a run is in progress), dry_run, applied, failed (control totals did not tie, or the run stopped: see `run.error`) or expired."},"amounts_in":{"type":"string","enum":["dollars","cents"],"title":"Amounts In","description":"The unit of every money column."},"line_key_field":{"anyOf":[{"type":"string","enum":["external_line_id","external_merchant_id"]},{"type":"null"}],"title":"Line Key Field","description":"Which key groups rows into lines: `external_line_id` when mapped, else `external_merchant_id`. Null when neither is mapped."},"mapping":{"additionalProperties":{"anyOf":[{"type":"string"},{"type":"null"}]},"type":"object","title":"Mapping","description":"Target field → source column in force."},"totals":{"$ref":"#/components/schemas/ImportBatchTotals","description":"What the file holds under the mapping in force."},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When the file was staged (UTC)."},"expires_at":{"type":"string","format":"date-time","title":"Expires At","description":"Seven days after staging. An unapplied batch is refused after it."},"applied_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Applied At","description":"When the file was written to the book."},"created_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By","description":"Who staged the file."},"last_run":{"anyOf":[{"$ref":"#/components/schemas/ImportRunSummary"},{"type":"null"}],"description":"The last finished dry run or apply, from its report; null before the first."},"run":{"anyOf":[{"$ref":"#/components/schemas/ImportRun"},{"type":"null"}],"description":"The current or last run, with its progress: poll this while `status` is `running`. Null before the first."},"result":{"anyOf":[{"$ref":"#/components/schemas/ProgramLoanImportResponse"},{"type":"null"}],"description":"Once the last run has finished: the body the inline call returns (per-line results, totals against the control totals, `applied`, `report_id`). Null while a run is in progress, after a run that stopped, and in the list."}},"type":"object","required":["staging_id","program_id","filename","sha256","row_count","status","amounts_in","created_at","expires_at"],"title":"ImportBatchResponse","description":"A staged loan file: identity, mapping, status and totals."},"ImportBatchTotals":{"properties":{"row_count":{"type":"integer","title":"Row Count","description":"Rows in the file.","default":0},"importable_count":{"type":"integer","title":"Importable Count","description":"Rows with no error.","default":0},"error_row_count":{"type":"integer","title":"Error Row Count","description":"Rows that will be rejected.","default":0},"warning_count":{"type":"integer","title":"Warning Count","description":"Warnings across every row.","default":0},"unassigned_row_count":{"type":"integer","title":"Unassigned Row Count","description":"Rows with no line key, so on no line.","default":0},"line_count":{"type":"integer","title":"Line Count","description":"Distinct line keys in the file.","default":0},"unmatched_line_count":{"type":"integer","title":"Unmatched Line Count","description":"Line keys with no line on the program.","default":0},"principal_cents":{"type":"integer","title":"Principal Cents","description":"Original principal over importable rows.","default":0},"outstanding_cents":{"type":"integer","title":"Outstanding Cents","description":"Outstanding over importable rows.","default":0}},"type":"object","title":"ImportBatchTotals","description":"What a staged file holds under its current mapping."},"ImportExpectedTotals":{"properties":{"count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Count","description":"Expected row count."},"principal_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Principal Cents","description":"Expected principal."},"outstanding_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Outstanding Cents","description":"Expected outstanding."}},"type":"object","title":"ImportExpectedTotals","description":"The control totals the operator gave from the partner's book."},"ImportLineDefaults":{"properties":{"credit_limit_cents":{"type":"integer","minimum":0.0,"title":"Credit Limit Cents","description":"The new line's limit."},"term_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Term Days","description":"Defaults to the program's first term."},"start_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Start Date","description":"The signing date of the merchant's agreement."}},"additionalProperties":false,"type":"object","required":["credit_limit_cents"],"title":"ImportLineDefaults","description":"Terms for a line the import opens for a key the program does not have."},"ImportLineResult":{"properties":{"line_key":{"type":"string","title":"Line Key","description":"The line key."},"credit_line_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Credit Line Id","description":"The line; null when none could be found or opened."},"created":{"type":"boolean","title":"Created","description":"The line was opened by this import.","default":false},"imported":{"type":"integer","title":"Imported","description":"New loans.","default":0},"duplicates":{"type":"integer","title":"Duplicates","description":"Rows whose loan was already on the line.","default":0},"rejected":{"type":"integer","title":"Rejected","description":"Rows that did not load.","default":0},"principal_cents":{"type":"integer","title":"Principal Cents","description":"Original principal over rows that loaded.","default":0},"outstanding_cents":{"type":"integer","title":"Outstanding Cents","description":"Outstanding over rows that loaded.","default":0},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Why the whole line did not load: no such line, or opening it was refused."}},"type":"object","required":["line_key"],"title":"ImportLineResult","description":"What the import did on one line."},"ImportRowRejection":{"properties":{"row_number":{"type":"integer","title":"Row Number","description":"Position in the file, from 1."},"line_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Line Key","description":"The row's line key."},"external_settlement_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Settlement Id","description":"The row's issuer transaction id, when it has one."},"reason":{"type":"string","title":"Reason","description":"Why it was rejected."}},"type":"object","required":["row_number","reason"],"title":"ImportRowRejection","description":"A row that did not load, and why."},"ImportRun":{"properties":{"id":{"type":"string","title":"Id","description":"The run. A new start gets a new id."},"kind":{"type":"string","enum":["dry_run","apply"],"title":"Kind","description":"`dry_run` or `apply`."},"mode":{"type":"string","enum":["sync","async"],"title":"Mode","description":"`sync`: ran in the request. `async`: ran as a background job."},"started_at":{"type":"string","format":"date-time","title":"Started At","description":"When it was started (UTC)."},"finished_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Finished At","description":"When it finished or stopped; null while running."},"heartbeat_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Heartbeat At","description":"The job's last sign of life (UTC)."},"elapsed_seconds":{"type":"integer","title":"Elapsed Seconds","description":"Seconds from start to finish, or to now while it runs."},"stale":{"type":"boolean","title":"Stale","description":"Still `running`, but the job has not reported for 10 minutes: its worker stopped. Starting the batch again takes it over.","default":false},"started_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started By","description":"Who started it."},"inputs":{"$ref":"#/components/schemas/ImportRunInputs","description":"What the run was started with: control totals, as-of date, line options."},"progress":{"$ref":"#/components/schemas/ImportRunProgress","description":"How far it got: rows and lines done of the total, and the phase."},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Why the run stopped before finishing (a crash, a timeout, or a run taken over). Nothing was written; run it again."}},"type":"object","required":["id","kind","mode","started_at","elapsed_seconds","progress"],"title":"ImportRun","description":"A batch's current or last dry run or apply."},"ImportRunInputs":{"properties":{"as_of":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"As Of","description":"Loan status assessed as at this date; null is today."},"expected_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expected Count","description":"Control count."},"expected_principal_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expected Principal Cents","description":"Control principal."},"expected_outstanding_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expected Outstanding Cents","description":"Control outstanding."},"create_missing_lines":{"type":"boolean","title":"Create Missing Lines","description":"Lines are opened for keys the program lacks.","default":false},"line_defaults":{"anyOf":[{"$ref":"#/components/schemas/ImportLineDefaults"},{"type":"null"}],"description":"Terms for the lines the run opens."},"allow_cross_line_duplicates":{"type":"boolean","title":"Allow Cross Line Duplicates","description":"Rows repeating a transaction id held on another line load.","default":false}},"type":"object","title":"ImportRunInputs","description":"What the run was asked to do: the options of the request that started it."},"ImportRunProgress":{"properties":{"phase":{"type":"string","enum":["queued","loading","importing","checking","saving","done","failed"],"title":"Phase","description":"`queued` (waiting for a worker), `loading` (reading the staged rows), `importing` (line by line), `checking` (reading the book back for the report), `saving`, then `done`, or `failed`."},"rows_done":{"type":"integer","title":"Rows Done","description":"Rows processed so far.","default":0},"rows_total":{"type":"integer","title":"Rows Total","description":"Rows in the file.","default":0},"lines_done":{"type":"integer","title":"Lines Done","description":"Lines processed so far.","default":0},"lines_total":{"type":"integer","title":"Lines Total","description":"Distinct line keys in the file.","default":0}},"type":"object","required":["phase"],"title":"ImportRunProgress","description":"How far a dry run or apply has got."},"ImportRunSummary":{"properties":{"dry_run":{"type":"boolean","title":"Dry Run","description":"The run was a dry run."},"applied":{"type":"boolean","title":"Applied","description":"The run wrote to the book."},"run_at":{"type":"string","format":"date-time","title":"Run At","description":"When it ran (UTC)."},"run_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Run By","description":"Who ran it."},"mismatches":{"items":{"type":"string"},"type":"array","title":"Mismatches","description":"Everything that did not tie: control totals, then lines. After an apply the control totals refused, a last entry says nothing was written and what to do."}},"type":"object","required":["dry_run","applied","run_at"],"title":"ImportRunSummary","description":"The last dry run or apply of a batch."},"ImportTotals":{"properties":{"count":{"type":"integer","title":"Count","description":"Rows that loaded: new plus duplicates.","default":0},"principal_cents":{"type":"integer","title":"Principal Cents","description":"Original principal.","default":0},"outstanding_cents":{"type":"integer","title":"Outstanding Cents","description":"Outstanding.","default":0}},"type":"object","title":"ImportTotals","description":"Counts and money over the rows that loaded (or would)."},"ImportWarning":{"properties":{"code":{"type":"string","title":"Code","description":"`no_interest_ceiling`: the program has no ceiling on a loan's interest, and the ceiling is frozen onto each loan when it is created, so these loans stay uncapped for good. `repeats_id_on_other_line`: the row loaded although its transaction id is already on another line."},"message":{"type":"string","title":"Message","description":"What it means and what to do, in words."},"row_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Row Number","description":"Position in the file, from 1; null for the run."},"line_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Line Key","description":"The row's line key."},"external_settlement_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Settlement Id","description":"The row's issuer transaction id."}},"type":"object","required":["code","message"],"title":"ImportWarning","description":"Something the operator should read before applying; the run did not stop."},"ProgramLoanImportRequest":{"properties":{"staging_id":{"type":"string","title":"Staging Id","description":"The batch from the parse step."},"dry_run":{"type":"boolean","title":"Dry Run","description":"Run every line and report, then write nothing. On by default.","default":true},"as_of":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"As Of","description":"Loan status is assessed as at this date. Today."},"expected_count":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Expected Count","description":"Rows the source file holds. A different count writes nothing."},"expected_principal_cents":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Expected Principal Cents","description":"Original principal summed over the file."},"expected_outstanding_cents":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Expected Outstanding Cents","description":"Outstanding summed over the file: principal, fee and accrued interest, less what was paid or credited; written-off loans count as zero."},"create_missing_lines":{"type":"boolean","title":"Create Missing Lines","description":"Open a line for each key the program does not have yet.","default":false},"line_defaults":{"anyOf":[{"$ref":"#/components/schemas/ImportLineDefaults"},{"type":"null"}],"description":"Terms for the lines `create_missing_lines` opens."},"mode":{"type":"string","enum":["sync","async","auto"],"title":"Mode","description":"`sync` runs the file in the request and answers with the result. `async` queues it as a background job and answers `202` with the batch (`status: running`); poll `GET /credit/import-batches/{id}`. `auto` (the default) runs files of up to 1,000 rows in the request and larger ones as a job. `sync` is refused over 2,000 rows.","default":"auto"},"allow_cross_line_duplicates":{"type":"boolean","title":"Allow Cross Line Duplicates","description":"A row whose original transaction id is already on a different line of the program is rejected (most likely a loan imported before under another merchant key), on a dry run as on an apply. Set true to load such rows anyway; each is listed in `warnings`.","default":false},"dry_run_token":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Dry Run Token","description":"On an apply: the `dry_run_token` of the dry run you checked. The apply then runs only with exactly that dry run's inputs (`as_of`, the expected totals, `create_missing_lines`, `line_defaults`) under the mapping it ran with, and it must be the batch's last dry run; otherwise `400` `CREDIT_690` and nothing runs. Optional."}},"additionalProperties":false,"type":"object","required":["staging_id"],"title":"ProgramLoanImportRequest","description":"Dry-run or apply a staged file across the whole program."},"ProgramLoanImportResponse":{"properties":{"staging_id":{"type":"string","title":"Staging Id","description":"The batch."},"report_id":{"type":"string","title":"Report Id","description":"The import report: `GET /credit/import-batches/{id}/report`."},"dry_run":{"type":"boolean","title":"Dry Run","description":"Nothing was meant to be written."},"applied":{"type":"boolean","title":"Applied","description":"Every line was written together. False on a dry run, and when a control total did not match: then nothing was written."},"as_of":{"type":"string","format":"date","title":"As Of","description":"The date loan status was assessed as at."},"lines":{"items":{"$ref":"#/components/schemas/ImportLineResult"},"type":"array","title":"Lines","description":"What the run did on each line."},"lines_created":{"type":"integer","title":"Lines Created","description":"Lines opened by the import.","default":0},"imported":{"type":"integer","title":"Imported","description":"New loans over every line.","default":0},"duplicates":{"type":"integer","title":"Duplicates","description":"Duplicates over every line.","default":0},"rejected":{"type":"integer","title":"Rejected","description":"Rejected rows over the file.","default":0},"totals":{"$ref":"#/components/schemas/ImportTotals","description":"What loaded (or would): checked against `expected`."},"expected":{"$ref":"#/components/schemas/ImportExpectedTotals","description":"The control totals the run was given."},"mismatches":{"items":{"type":"string"},"type":"array","title":"Mismatches","description":"Each control total that did not match the file, in words. On an apply they refused, a last entry says that nothing was written and what to do: fix or leave out the rejected rows, or correct the totals, then run the dry run again."},"rejections":{"items":{"$ref":"#/components/schemas/ImportRowRejection"},"type":"array","title":"Rejections","description":"Rejected rows, the first 200; `rejected` counts them all. Every one is in the report (`GET /credit/import-batches/{id}/report`), which is available after a dry run too."},"warnings":{"items":{"$ref":"#/components/schemas/ImportWarning"},"type":"array","title":"Warnings","description":"Things to read before applying: run-level warnings first (a program with no interest ceiling), then rows. The first 200; `warning_count` has them all."},"warning_count":{"type":"integer","title":"Warning Count","description":"Every warning of the run.","default":0},"dry_run_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dry Run Token","description":"On a dry run: a digest of the batch, its mapping and this run's inputs (with `as_of` resolved). Send it on the apply as `dry_run_token` to apply exactly what this dry run checked. Null on an apply."}},"type":"object","required":["staging_id","report_id","dry_run","applied","as_of"],"title":"ProgramLoanImportResponse","description":"The outcome of a program-level dry run or apply."},"SuccessEnvelope_ImportBatchResponse_":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"},"data":{"anyOf":[{"$ref":"#/components/schemas/ImportBatchResponse"},{"type":"null"}]}},"additionalProperties":true,"type":"object","title":"SuccessEnvelope[ImportBatchResponse]"},"SuccessEnvelope_ProgramLoanImportResponse_":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"},"data":{"anyOf":[{"$ref":"#/components/schemas/ProgramLoanImportResponse"},{"type":"null"}]}},"additionalProperties":true,"type":"object","title":"SuccessEnvelope[ProgramLoanImportResponse]"},"ErrorResponse":{"title":"ErrorResponse","type":"object","description":"Envelope returned by every non-2xx response. Branch on `code` (stable) rather than `message` (human-readable, may change).","required":["error","message","code"],"properties":{"error":{"type":"string","description":"Error category: `validation_error`, `unauthorized`, `forbidden`, `not_found`, `conflict`, `rate_limited`, `server_error`, or `service_unavailable`."},"message":{"type":"string","description":"Human-readable explanation, safe to show to end users."},"code":{"type":"string","description":"Stable machine-readable code in `CATEGORY_NNN` form (e.g. `NOT_FOUND_006`, `AUTH_010`, `GEN_002`)."},"request_id":{"type":"string","description":"Correlation id for support requests. Echoes the `x-request-id` request header when one was supplied."},"errors":{"type":"object","additionalProperties":{"type":"string"},"description":"Field-level validation messages keyed by field name. Present on 400/422 validation failures only."}}}},"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Token","description":"API token authentication. Format: `Bearer dz_...`"}}},"security":[{"BearerAuth":[]}]}