Back to Index Live Mode Enroll Mode CLI Reference

Live Mode API Reference

All endpoints are under /api/v1/. The server is started with meld serve --config config.yaml --port 8090.

For enroll-mode endpoints (meld enroll), see Enroll Mode.

Endpoints

Method Path Description
POST /a/add Add or update an A-side record, return top matches from B
POST /b/add Add or update a B-side record, return top matches from A
POST /a/add-batch Add or update multiple A-side records in one request
POST /b/add-batch Add or update multiple B-side records in one request
POST /a/match Score an A-side record against B without storing it (read-only)
POST /b/match Score a B-side record against A without storing it (read-only)
POST /a/match-batch Score multiple A-side records against B without storing (read-only)
POST /b/match-batch Score multiple B-side records against A without storing (read-only)
POST /a/remove Remove an A-side record from all indices and break any crossmap pair
POST /b/remove Remove a B-side record from all indices and break any crossmap pair
POST /a/remove-batch Remove multiple A-side records in one request
POST /b/remove-batch Remove multiple B-side records in one request
GET /a/query?id=X Look up an A-side record and its crossmap status
GET /b/query?id=X Look up a B-side record and its crossmap status
POST /crossmap/confirm Confirm a match (add to cross-map)
POST /crossmap/break Break a confirmed match (remove from cross-map)
GET /crossmap/lookup?id=X&side=a Look up whether a record has a confirmed match
GET /crossmap/pairs Export all confirmed crossmap pairs (paginated)
GET /crossmap/stats Coverage statistics (matched/unmatched counts per side)
GET /a/unmatched List A-side record IDs with no crossmap pair (paginated)
GET /b/unmatched List B-side record IDs with no crossmap pair (paginated)
GET /review/list List pending review-band matches (paginated)
POST /exclude Exclude a pair (known non-match). Breaks existing match if present
DELETE /exclude Remove an exclusion, allowing the pair to match again
GET /health Health check
GET /status Detailed server status (record counts, uptime)

[!IMPORTANT] Live mode treats A and B sides identically. Adding, removing, querying, and matching records works the same way on both sides — same operations, same scoring logic, same match semantics.

Adding a record

When you add a record to one side, the melder immediately encodes it, searches the opposite side for matches, and returns the top results. If the record already exists (same ID), it is updated and re-matched.

Request:

POST /api/v1/a/add

{
  "record": {
    "entity_id": "ENT-001",
    "legal_name": "Acme Corporation",
    "country_code": "US"
  }
}

Response:

{
  "id": "ENT-001",
  "status": "added",
  "matches": [
    {
      "id": "CP-042",
      "score": 0.91,
      "classification": "auto",
      "field_scores": [...]
    }
  ]
}

The status field will be "added" for new records or "updated" for existing records that were modified.

Removing a record

Remove a record by ID. This removes it from all indices (embedding, blocking, unmatched set) and breaks any existing crossmap pair. The opposite-side record in a broken pair is returned to the unmatched pool.

Request:

POST /api/v1/a/remove

{
  "id": "ENT-001"
}

Response:

{
  "status": "removed",
  "id": "ENT-001",
  "side": "a",
  "crossmap_broken": ["CP-042"]
}

The crossmap_broken array lists any opposite-side IDs whose pairing was broken by the removal. It is omitted when empty.

Batch operations

The batch endpoints accept multiple records in a single request. This amortises the ONNX encoding cost across the batch — all texts are encoded in a single model call, then scored sequentially. Maximum 1000 records per request. Empty arrays return 400.

Add batch

Add or update multiple records at once:

POST /api/v1/a/add-batch

{
  "records": [
    {"entity_id": "ENT-001", "legal_name": "Acme Corp", "country_code": "US"},
    {"entity_id": "ENT-002", "legal_name": "Globex Inc", "country_code": "GB"}
  ]
}

Response:

{
  "results": [
    {"id": "ENT-001", "status": "added", "matches": [...], ...},
    {"id": "ENT-002", "status": "added", "matches": [...], ...}
  ]
}

Each entry in results has the same shape as a single /add response. If a record fails (e.g. missing ID field), its entry has "status": "error" with a message — the rest of the batch still succeeds.

Match batch

Score multiple records without storing them:

POST /api/v1/b/match-batch

{
  "records": [
    {"counterparty_id": "CP-X", "counterparty_name": "Test Corp", "domicile": "US"},
    {"counterparty_id": "CP-Y", "counterparty_name": "Another Inc", "domicile": "GB"}
  ]
}

Remove batch

Remove multiple records by ID:

POST /api/v1/a/remove-batch

{
  "ids": ["ENT-001", "ENT-002", "NONEXISTENT"]
}

Response:

{
  "results": [
    {"id": "ENT-001", "side": "a", "status": "removed"},
    {"id": "ENT-002", "side": "a", "status": "removed"},
    {"id": "NONEXISTENT", "side": "a", "status": "not_found"}
  ]
}

Missing IDs produce "status": "not_found" entries rather than failing the request.

Throughput

Batch endpoints are faster than sending single requests in a loop because they amortise ONNX encoding overhead. On the 10K x 10K benchmark dataset:

Batch size Throughput (rec/s) Speedup vs single
1 221 0.9x
10 331 1.4x
50 445 1.8x
100 319 1.3x
500 325 1.3x

Batch size 50 is the sweet spot — large enough to amortise encoding, small enough that per-batch latency stays under 200ms.

Querying a record

Look up a record by ID to see its full contents and crossmap status without modifying anything.

GET /api/v1/a/query?id=ENT-001

Response:

{
  "id": "ENT-001",
  "side": "a",
  "record": {
    "entity_id": "ENT-001",
    "legal_name": "Acme Corporation",
    "country_code": "US"
  },
  "crossmap": {
    "status": "matched",
    "paired_id": "CP-042",
    "paired_record": {
      "counterparty_id": "CP-042",
      "counterparty_name": "ACME Corp"
    }
  }
}

If the record is unmatched, crossmap.status is "unmatched" and the paired_id and paired_record fields are omitted. Returns 404 if the record ID is not found.

Crossmap operations

Export pairs

List all confirmed crossmap pairs, with optional pagination:

GET /api/v1/crossmap/pairs?offset=0&limit=100
{
  "total": 4523,
  "offset": 0,
  "pairs": [
    { "a_id": "ENT-001", "b_id": "CP-042" },
    { "a_id": "ENT-007", "b_id": "CP-119" }
  ]
}

Coverage statistics

GET /api/v1/crossmap/stats
{
  "records_a": 10000,
  "records_b": 9500,
  "crossmap_pairs": 4523,
  "matched_a": 4523,
  "matched_b": 4523,
  "unmatched_a": 5477,
  "unmatched_b": 4977,
  "coverage_a": 0.4523,
  "coverage_b": 0.4761
}

Unmatched records

List record IDs that have no crossmap pair on a given side. Supports pagination and an optional include_records=true parameter to return full record data alongside each ID.

GET /api/v1/a/unmatched?offset=0&limit=50
GET /api/v1/b/unmatched?include_records=true&limit=10

Response (without include_records):

{
  "side": "a",
  "total": 5477,
  "offset": 0,
  "records": [
    { "id": "ENT-003" },
    { "id": "ENT-009" }
  ]
}

Response (with include_records=true):

{
  "side": "b",
  "total": 4977,
  "offset": 0,
  "records": [
    {
      "id": "CP-055",
      "record": {
        "counterparty_id": "CP-055",
        "counterparty_name": "Initech LLC",
        "domicile": "US"
      }
    }
  ]
}

Review list

List pending review-band matches — pairs that scored between review_floor and auto_match during upsert but were not auto-confirmed. Resolution happens via the /crossmap/confirm and /crossmap/break endpoints.

GET /api/v1/review/list?offset=0&limit=20
{
  "total": 37,
  "offset": 0,
  "reviews": [
    {
      "id": "ENT-012",
      "side": "a",
      "candidate_id": "CP-088",
      "score": 0.74
    },
    {
      "id": "CP-201",
      "side": "b",
      "candidate_id": "ENT-055",
      "score": 0.69
    }
  ]
}

Reviews are sorted by score descending (highest-confidence pairs first). Confirming or breaking a pair removes it from the review queue. Re-upserting a record also clears its stale review entries.

Excluding a pair (known non-match)

Mark a pair of records as a known non-match. Excluded pairs are never scored or returned as candidates, even if they would otherwise match.

If the pair is currently matched to each other in the crossmap, the match is broken automatically before the exclusion is applied.

POST /api/v1/exclude
{ "a_id": "ENT-012", "b_id": "CP-088" }

Response:

{
  "excluded": true,
  "match_was_broken": true,
  "a_id": "ENT-012",
  "b_id": "CP-088"
}

match_was_broken is true if the pair was previously matched and the match was broken as part of this operation. After excluding, both records become unmatched and can be re-matched to other records on the next upsert or try-match.

Exclusions survive server restarts (persisted via WAL and flushed to the exclusions CSV on shutdown).

Removing an exclusion

Remove an exclusion, allowing the pair to match again.

DELETE /api/v1/exclude
{ "a_id": "ENT-012", "b_id": "CP-088" }

Response:

{
  "removed": true,
  "a_id": "ENT-012",
  "b_id": "CP-088"
}

removed is true if the pair was previously excluded and has now been removed. false if the pair was not in the exclusion set.

After removing an exclusion, the pair will be scored normally on the next upsert or try-match. It is not automatically re-scored — you need to re-upsert one of the records to trigger matching.