Skip to content

Enrichment: File Match

If you only want to match and enrich a file — no segments, no audiences, no campaigns — this is the endpoint family built for that. Every route here is namespaced under /v1/match/, and that’s deliberate: stay inside these routes and you never need to learn the audience-building model at all.

  1. POST /v1/match/file — creates the job, returns a signed upload URL.
  2. PUT your file to that URL.
  3. Poll GET /v1/match/file/{match_id} until matching finishes.
  4. POST /v1/match/{match_id}/deliveries — export the result.
  5. Poll GET /v1/match/{match_id}/deliveries until the delivery completes, then download.

That’s the entire happy path. Everything below fills in the details and the five ways people get it wrong.

Terminal window
curl -X POST https://api.infiniteaudience.ai/v1/match/file \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Q1 customer list", "file_format": "csv"}'
{
"segment_id": "seg_8f2a91",
"audience_id": "aud_c103d4",
"match_id": "seg_8f2a91",
"name": "Q1 customer list",
"status": "pending",
"upload_url": "https://storage.googleapis.com/cf-uploads/enrichment-uploads/org_xyz/seg_8f2a91/input.csv?X-Goog-Signature=...",
"upload_expires_at": "2026-08-10T15:30:00.000Z",
"expires_at": "2026-11-08",
"file_format": "csv",
"compression": "none",
"shard_count": 1
}

PUT the file directly to upload_url — no Authorization header, since the URL itself is the credential:

Terminal window
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: text/csv" \
--data-binary @customers.csv

The Content-Type you send must exactly match the format you declared:

file_format Required Content-Type
csv text/csv
avro application/avro
json application/json
jsonl application/x-ndjson

Gzip: send the raw gzip bytes with the underlying format’s Content-Type (e.g. csv + gzip is still Content-Type: text/csv, just gzip-compressed bytes) and set compression: "gzip" in the create request — do not set an HTTP Content-Encoding header. avro + gzip is rejected with 400 UNSUPPORTED_INPUT_COMPRESSION — Avro is already internally compressed; upload it uncompressed, or use csv/json/jsonl if you need gzip.

Sharding large files: set shard_count (1–50) on the create call to get upload_urls[] instead of a single upload_urlPUT every shard. Every CSV shard must repeat the same header row, and all shards must share format and compression.

The upload itself is what starts identity resolution — nothing else triggers it.

Terminal window
curl https://api.infiniteaudience.ai/v1/match/file/seg_8f2a91 \
-H "Authorization: Bearer $ACCESS_TOKEN"
{
"match_id": "seg_8f2a91",
"name": "Q1 customer list",
"status": "active",
"matching_status": "completed",
"input_record_count": 50000,
"match_count": 34210,
"match_rate": 0.6842,
"amount_charged": null,
"file_format": "csv",
"compression": "none",
"column_mappings": { "email": "email_address", "first_name": "fname" },
"error_message": null,
"segment_id": "seg_8f2a91",
"audience_id": "aud_c103d4",
"audience_status": "active",
"source_job_id": "seg_8f2a91",
"created_at": "2026-08-10T15:02:11.000Z",
"expires_at": "2026-11-08"
}

Poll until matching_status is completed or failedpending and processing both mean “still running.” Check status, too: it flips to active in the same instant as matching_status: completed, so either is a valid ready-signal.

amount_charged is null here — that’s correct, not a bug. File match bills at first delivery, not at upload; see the billing note in step 4.

Terminal window
curl -X POST https://api.infiniteaudience.ai/v1/match/seg_8f2a91/deliveries \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"include_unmatched": true}'
{
"delivery_id": "del_44a1",
"audience_id": "aud_c103d4",
"audience_version": 1,
"destination": "download",
"status": "processing",
"output_format": "csv",
"output_compression": "none",
"record_count": null
}

Optionally pass template_id or field_list to choose which enriched attributes to include — omit both for the Standard IAG attribute set. output_format defaults to your input format; override it if you want a different one out than you put in.

Billing: this is where file match actually charges. The first successful delivery for a given match run emits one aggregate usage event covering matching + enrichment + delivery together; re-delivering the exact same (job × version × fields × destination) again is free.

5. Poll for delivery completion and download

Section titled “5. Poll for delivery completion and download”
{
"deliveries": [
{
"delivery_id": "del_44a1",
"audience_id": "aud_c103d4",
"status": "completed",
"output_format": "csv",
"output_compression": "none",
"output_bytes": 4821004,
"output_record_count": 50000,
"completed_at": "2026-08-10T15:06:44.000Z",
"download_urls": ["https://storage.googleapis.com/cf-exports/.../output.csv?X-Goog-Signature=..."]
}
],
"total": 1
}

Match on delivery_id — it’s a list, most recent first. download_urls are signed for 24 hours; re-GET this same endpoint any time to get freshly re-signed URLs rather than letting old ones expire on you.


Human-in-the-loop: reviewing column mappings before matching starts

Section titled “Human-in-the-loop: reviewing column mappings before matching starts”

By default, uploading immediately triggers automatic column-name inference. If you’d rather review the mapping first — useful for files with unusual or ambiguous column names — set hitl: true on the create call. This changes the flow:

  1. POST /v1/match/file with {"hitl": true} — the upload URL now points to a staging location that automatic matching doesn’t watch.

  2. Upload as normal. Nothing starts yet.

  3. POST /v1/match/{match_id}/analyze (empty body) to preview the inferred mapping:

    {
    "segment_id": "seg_8f2a91",
    "analysis": {
    "columns": [
    { "source_column": "email_address", "mapped_to": "email", "inferred_type": "STRING", "confidence": 0.98, "warning": null },
    { "source_column": "fname", "mapped_to": "first_name", "inferred_type": "STRING", "confidence": 0.72, "warning": "Ambiguous — verify" }
    ],
    "unmapped_columns": ["internal_notes"],
    "recommendations": ["Consider mapping 'internal_notes' or confirm it should be excluded."]
    }
    }

    Treat anything under 0.6 confidence as needing a human look before you confirm.

  4. POST /v1/segments/{match_id}/mappings with {"column_mappings": {...}} to confirm and start matching — note this one route is segment-namespaced, not match-namespaced; there is no /v1/match/{id}/mappings twin. Send the complete mapping, not a delta — omit a field entirely (rather than mapping it) using the "(skip)" sentinel value.

    Changed your mind? POST /v1/segments/{match_id}/mappings/cancel clears the staged upload without starting anything — fully idempotent, safe to call even if you already confirmed.

  5. Continue from step 3 of the main flow above (poll for completion, then deliver).

Recapping the mechanics from steps 2 and 5, all in one place:

  • Upload URLs: 30-minute TTL, Content-Type must exactly match the declared format (see table above), gzip is raw bytes with no Content-Encoding header, avro+gzip is rejected.
  • Download URLs: 24-hour TTL, always re-signed fresh by GET /v1/match/{id}/deliveries — never cached from an earlier response.
  • Sharding: shard_count 1–50 on create; upload_urls[] replaces upload_url when sharded (never both); every shard must be uploaded, and every CSV shard repeats the header row.