Bulk Delivery

Export every record matching a list query to a single file — NDJSON, JSON, CSV, or Parquet. Bulk Delivery queues a background job that dumps the full result set to S3 and hands back a signed download URL. You're billed by the number of rows delivered.

Bulk Delivery exports an entire filtered dataset to a single file. Instead of paging through millions of records 100 at a time, you submit the same filters you'd use on a list endpoint, pick an output format, and the API runs a background job that writes every matching row to a file in S3. Poll the job, and when it's done you get a signed URL to download the result.

It's the right tool when you want a whole slice of the dataset — every loan in a state, every originator at a company, every property in a set of ZIPs — landed as one file for a warehouse load, an offline analysis, or a hand-off to another system. For interactive, page-at-a-time reads, use the list endpoints instead.

Bulk Delivery is available for six resources:

ResourceSubmitPoll
AgentsPOST /v1/agents/bulk-deliveryGET /v1/agents/bulk-delivery/{jobId}
OriginatorsPOST /v1/originators/bulk-deliveryGET /v1/originators/bulk-delivery/{jobId}
CompaniesPOST /v1/companies/bulk-deliveryGET /v1/companies/bulk-delivery/{jobId}
LoansPOST /v1/loans/bulk-deliveryGET /v1/loans/bulk-delivery/{jobId}
PropertiesPOST /v1/properties/bulk-deliveryGET /v1/properties/bulk-delivery/{jobId}
BranchesPOST /v1/branches/bulk-deliveryGET /v1/branches/bulk-delivery/{jobId}

Every endpoint behaves identically — only the entity and its columns differ. The examples below use agents; swap the path segment for any other resource.

Bulk Delivery requires the bulk-delivery.<resource> entitlement (e.g. bulk-delivery.agents) on your organization. It's granted per resource and controls both access and which columns you're allowed to export. If you don't have it, submission returns 403. Contact support to enable it.

How credits work

Bulk Delivery is metered by the number of rows delivered — a bigger export costs more, scaling with the row count.

  • You're gated on the estimate at submission. Before queuing the job, the API estimates the result-set size (estimatedTotal) and requires enough balance to cover it. If you can't cover the worst case, submission returns 402 — the job never starts.
  • You're charged on the actual delivered count. Billing lands when the job completes, against the rows actually written (chargedCount), which can be lower than the estimate.
  • A failed job isn't charged. If the export fails, no credit is debited.

The completed job record reports exactly what you paid: chargedCount (rows billed), creditsCharged (credits debited), and creditLedgerId (the ledger entry).

Submit a job

Send a POST to the resource's bulk-delivery endpoint. The request body is the same filter shape as the list endpointflatFilters, advancedFilters, period, and sort all work exactly as documented in Pagination & Filtering — plus two delivery-specific fields:

curl -X POST https://api.modelmatch.com/v1/agents/bulk-delivery \
  -H "x-api-key: mm_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "flatFilters": { "state": "CA" },
    "format": "parquet",
    "fields": ["fullName", "city", "state", "transactionCount"]
  }'
Request fieldTypeDescription
formatstringRequired. Output format — ndjson, json, csv, or parquet. See Formats.
fieldsstring[]Optional column projection. Names are the camelCase response fields the list endpoint returns (e.g. city, interestRate). Intersected with your entitlement's allowed columns and the entity's full schema; if the intersection is empty, all allowed columns are delivered.
flatFilters / advancedFiltersDefine the result set, same as a list query.
periodstringOptional time-window scope ("2024", "last12Months", "yearToDate", "allTime", …).
sortarrayOptional ordering of rows in the output file.

Don't supply pagination — a job always returns the full matching set. Unlike a list call, you don't page through results: the filters define the slice and the entire slice is written to one file. That's the whole point of the feature.

A successful submission returns 202 Accepted with a job handle:

{
  "jobId": "job_abc123",
  "status": "queued",
  "entityType": "agents",
  "format": "parquet",
  "estimatedTotal": 18452,
  "maxRows": 100000
}
FieldDescription
jobIdHandle to poll for status.
statusqueued on submission.
entityTypeThe resource being exported.
formatEchoes the requested format.
estimatedTotalEstimated rows in the result set. Drives the credit gate — you need enough balance to cover it.
maxRowsThe hard ceiling for a single delivery. If your estimate exceeds it, submission is rejected before the job starts — see below.

Row limits

A single delivery is capped at maxRows. If the estimated result set is larger, the API rejects the request with 400 and a delivery_exceeds_max_rows error rather than queuing a job you can't complete:

{
  "error": "delivery_exceeds_max_rows",
  "estimatedTotal": 820000,
  "maxRows": 100000
}

Tighten your filters or split the export by a dimension (state, period, ZIP range) and submit multiple jobs.

Poll for status

Poll the GET endpoint with your jobId until status is completed (or failed):

curl https://api.modelmatch.com/v1/agents/bulk-delivery/job_abc123 \
  -H "x-api-key: mm_your_key_here"
{
  "jobId": "job_abc123",
  "entityType": "agents",
  "format": "parquet",
  "status": "completed",
  "estimatedTotal": 18452,
  "processed": 18452,
  "bytes": 7340032,
  "chargedCount": 18452,
  "creditsCharged": 185,
  "creditLedgerId": "ledg_xyz789",
  "startedAt": "2026-06-25T18:40:00.000Z",
  "completedAt": "2026-06-25T18:43:12.000Z",
  "resultsUrl": "https://..."
}
FieldDescription
statusqueued, running, completed, or failed.
processedRows written so far — track it against estimatedTotal for progress.
bytesSize of the output written so far.
chargedCountDelivered rows that were billed. Present once the job completes.
creditsChargedCredits debited for the delivered rows. Absent until completion; absent if the billing call didn't land.
creditLedgerIdLedger entry id for the debit. Absent if billing didn't land.
startedAt / completedAtISO-8601 job timestamps.
resultsUrlThe signed download URL. Present once completed.
errorFailure reason. Present only when status is failed.

resultsUrl is re-signed on every status fetch, so always download using the URL from your most recent poll — an older signed URL will expire. Job records (and their files) are retained for 30 days; after that, GET /bulk-delivery/{jobId} returns 404.

Download the result

Once status is completed, fetch resultsUrl directly — it's a plain signed S3 link, no auth header needed:

curl -L "$(curl -s https://api.modelmatch.com/v1/agents/bulk-delivery/job_abc123 \
  -H 'x-api-key: mm_your_key_here' | jq -r .resultsUrl)" \
  -o agents.parquet

Formats

Every format contains the same rows — the entity's list/summary record, one per row (optionally narrowed by fields). Pick based on how you'll consume the file:

formatMedia typeExtensionNotes
ndjsonapplication/x-ndjson.ndjson.gzGzipped newline-delimited JSON. Best for streaming row-by-row into a pipeline.
jsonapplication/json.jsonA single JSON array. Convenient for small-to-medium exports loaded whole.
csvtext/csv.csvRFC 4180. Best for spreadsheets and generic tabular tools.
parquetapplication/vnd.apache.parquet.parquetTyped columnar Apache Parquet. Scalar fields keep their types; nested fields are JSON-encoded strings. Best for warehouse loads and analytics.

Errors

All endpoints return the standard authentication errors (401, 403, 429). In addition:

StatusEndpointMeaning
400submitValidation failure, or the estimated result set exceeds maxRows (delivery_exceeds_max_rows, with estimatedTotal and maxRows).
402submitInsufficient credits — your balance is below the estimated cost. Body includes balance and cost.
403submitYour organization lacks the bulk-delivery.<resource> entitlement. Body includes entitlementKey.
403statusThe job belongs to another caller — you can only poll your own jobs.
404statusJob not found, or expired past the 30-day retention window.
502submitThe job failed to launch (preflight, S3, or task-launch failure). Safe to retry.

A 402 is the one to handle gracefully — catch it, surface balance vs cost, and prompt the user to top up before retrying. For a 403 on submit, the resource isn't enabled for the org; reach out to support rather than retrying.

On this page