Skip to content

Search

POST /v1/search finds candidates in one collection. You write the query, the filter and the order; MasterDB validates them against the collection’s allow-list, composes the index query itself, and answers at most 50 signed rows with a receipt. MasterDB never supplies an order and never rewrites a request: a request it cannot answer exactly is refused with a reason.

samples/typescript/search.ts
// Search: your query, your filter, your sort — in one collection and one country.
//
// MASTERDB_KEY_FILE your retrieval key's private half (PKCS #8 PEM); its public half is the sandbox key you took in the AI Portal
// MASTERDB_SANDBOX_KEY_GRANT optional; that key's sandbox_key_grant (mdb_sbxk1.…), needed until the sandbox has seen the key once
// MASTERDB_API_URL optional; the sandbox by default
import assert from 'node:assert/strict';
import { createPrivateKey } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { SANDBOX, createEd25519Signer, createRetrievalClient } from '@masterdb/client';
const signer = createEd25519Signer(createPrivateKey(readFileSync(process.env.MASTERDB_KEY_FILE as string)));
const sandboxKeyGrant = process.env.MASTERDB_SANDBOX_KEY_GRANT;
const client = createRetrievalClient({ baseUrl: process.env.MASTERDB_API_URL ?? SANDBOX.retrieval, signer, ...(sandboxKeyGrant ? { sandboxKeyGrant } : {}) });
// Relevance order is a sort you ask for (`_text_match:desc`); a price filter is in the searched country's price.
const products = await client.POST('/v1/products/search', {
body: {
q: 'collection',
query_by: ['product_name', 'short_description'],
filter: { all: [{ country: 'GB' }, { price_amount: { lte: '400.00' } }, { on_sale: false }] },
sort_by: '_text_match:desc',
limit: 10,
},
});
if (products.error) throw new Error(`${products.error.code}: ${products.error.detail ?? ''}`);
for (const row of products.data.rows) console.log(row.record_id, row['product_name'], row['price_GB'], row['price_currency_GB']);
assert.ok(products.data.rows.length <= 10);
assert.ok(products.data.rows.every((row) => (row['countries'] as string[]).includes('GB')));
assert.ok(products.data.rows.every((row) => (row['price_GB'] as number) <= 400));
// The same API finds every type. Business & Brand files in Ireland, newest first:
const files = await client.POST('/v1/search', {
body: { collection: 'business_files', filter: { all: [{ country: 'IE' }] }, sort_by: 'published_at:desc', limit: 20 },
});
if (files.error) throw new Error(files.error.code);
for (const row of files.data.rows) console.log(row.record_id, row['business_name']);
assert.ok(files.data.rows.length > 0);
// At most 50 rows and no second page. If the answer is not in them, narrow the criteria and search again.
const narrower = await client.POST('/v1/search', {
body: { collection: 'products', filter: { all: [{ country: 'US' }, { vertical: 'fashion' }] }, sort_by: 'published_at:desc', limit: 50 },
});
if (narrower.error) throw new Error(narrower.error.code);
assert.ok(narrower.data.rows.every((row) => row['vertical'] === 'fashion'));
console.log(`${narrower.data.rows.length} fashion products in the US`);

Run it with MASTERDB_KEY_FILE (your retrieval key’s private half) and, against the sandbox, MASTERDB_SANDBOX_KEY_GRANT set to the grant the AI Portal answered with the key. Both samples send the grant as the MDB-Sandbox-Key header on every request — the TypeScript SDK’s sandboxKeyGrant, the Python signing helper’s MasterDBAuth(key, sandbox_key_grant=…) — so the sandbox admits a key it has not seen yet instead of refusing it 401 key_unknown. Once the key is registered the grant is no longer needed. MASTERDB_API_URL is optional: the sandbox by default.

{
"collection": "products",
"q": "trail running shoes",
"query_by": ["product_name", "tags"],
"filter": { "all": [ { "country": "US" }, { "price_amount": { "lte": "150.00" } }, { "availability": "available" } ] },
"sort_by": "price_amount:asc",
"limit": 25
}
Member
collection products, business_files, events, jobs or updates. Optional on a typed address — POST /v1/products/search, /v1/business-files/search, /v1/events/search, /v1/jobs/search, /v1/updates/search — and must match it if given.
q The text query, up to 512 characters. Default *: everything the filter admits.
query_by Which text fields q is matched against. Default: all of the collection’s text fields.
filter {"all": [clause, …]}: every clause must hold. Exactly one clause names the country (ISO 3166-1 alpha-2); none or two is filter_required. At most 20 clauses.
sort_by Mandatory. field:asc or field:desc, up to three, comma-separated. For relevance order write _text_match:desc. Without it: sort_required.
limit 1 to 50; default 50.

A clause is an object with one member, the field name, and either a value ({"on_sale": false}, shorthand for eq) or operators ({"price_amount": {"gte": "20.00", "lt": "80.00"}}). Strings take eq, ne, in and nin where the field allows them; money, numbers and timestamps take eq, lt, lte, gt and gte; locations take near with lat, lng and radius_km. Money in a filter is a decimal string. Fields, filters and sort keys lists every field of every collection with its operators, generated from the allow-lists the service enforces.

One country, and its prices. The country clause selects the rows published for that country. On products, price_amount and price_currency in a filter or sort mean that country’s price.

{
"retrieval_id": "r_…",
"collection": "products",
"country": "US",
"rows": [ { "record_id": "mdb_…", "business_uuid": "…", "type": "products", "product_name": "…", "price_US": 10.34, "price_currency_US": "USD", "adl_origin": "sha256:…", "adl_proj": "sha256:…", "adl_row_sig": "…", "adl_key_id": "…", "ai_policy_bits": { "ai_policy_version": 1, "ai_policy_schema": 2, "use": 79, "action": 35, "blocked": 0 } } ],
"receipt": { "payloadType": "application/vnd.masterdb.receipt.v3+json", "payload": "…", "signatures": [ … ] }
}
  • Each row is exactly what the projection wrote and signed, plus members set as it is served: ai_policy_bits (the business’s AI policy in force — what it permits and the contexts it blocks, bc1–bc10) and sponsored (present, and true, only on a paid placement). A product row carries the price for every country it is published in (price_GB, price_US, …) as signed; rows carry numbers, records carry decimal strings.
  • partial: true appears when the index stopped at its time budget and answered with what it had. Narrow the query.
  • The receipt lists every row served with its origin hash and signature (Receipts).
  • published_at on a row is Unix seconds; on a record it is an RFC 3339 timestamp.
  • Freshness. When the business declares how fresh records of this type are expected to be, the row also carries freshness_expectation — {"cadence": "daily"}, or {"cadence": "interval", "interval_hours": 12}; cadences hourly, daily, weekly, monthly (31 days), quarterly (92), yearly (366), interval. Like ai_policy_bits it is set as the row is served and is outside the row signature; the business’s sealed Business & Brand file (a fetch of its bf_… record, member freshness_expectation) is what proves it. A record is stale when it is older than its expectation — now minus published_at is more than the window — and never stale without one: isStale / is_stale in the verifier libraries. Say “updated 2 minutes ago”, or flag what is past its window, rather than presenting it as current.

Rows from a business that blocked your group are absent, and nothing says so (Blocking).

There is no pagination, no cursor and no “more”. If the answer is not in the 50 rows, the search was too wide: add a filter, narrow the price range, pick a category, and search again. This is deliberate: it keeps a search a search, and makes walking the whole corpus a matter of millions of separately billed requests. There is also no list endpoint and no batch fetch.

A search that returns at least one row is one query. A search that returns no rows is free and unrestricted beyond the ordinary rate limit — use it to explore. Refusals are never billed. See Usage and billing.

A row is short by design. For everything the business sealed — descriptions, all prices, opening hours, how it wants to be represented, its sealed AI policy — fetch the record.