Download OpenAPI specification:
What a business's own systems call to publish, read back and audit what they published.
The business machine API, for a business's own
integration, a Shopify-style connector or an agency's feed — ADL Path A. There is no
bearer token: every request is signed with RFC 9421 by an integration key the business
generated and registered (tag="mdb-push"), and what authorises that key is a
publishing mandate an admin sealed with their passkey, scoped to record types and
countries and valid for at most 92 days.
Reads — the catalogue read-back, push attempts and analytics — are signed with the tag
mdb-business-read instead, and need a registered key but no mandate.
Sealed, or not published: every record is sealed by the business, a batch with
one Merkle root, and a record whose seal does not verify is refused with a reason that
names the seal. A push replaces the record; there is no merge on this path. Nothing
publishes without a seal, and a bare DELETE is refused.
Publishes a batch of records of one type, each exactly the bytes the business sealed,
under one batch seal: a Merkle root over the records' raw bytes signed with the
business's own key, carrying the per-key sequence number seq that must be greater
than the last accepted one (so a stolen key cannot roll a price back by replaying an
old seal) and a sealed_at within five minutes of receipt. Each record is validated
with every reason at once and answered accepted, updated or rejected; a push
replaces the record it names. A push that would move more than a fifth of the
catalogue's prices is held for the owner (the price-shock hold). Only products is published through this route.
Every record is also held to the scope rule (text naming another company or brand,
or directing how other sources are treated, is scope_violation; a brand the business
sells belongs in its Business & Brand Identity file's brands_sold) and to a malware and phishing check (url_flagged), and a display_domain must be its destination_url's own host
(display_domain_mismatch) — each a per-record rejection, never the batch's.
Back-pressure: the per-key caps stamped on the mandate — records per
hour and bytes per day — are judged before the body is parsed, and a request over
either is answered 429 rate_limited with Retry-After. A mandate with a source
allow-list (CIDR ranges and AS numbers) is honoured only from inside it
(403 source_not_allowed).
Accepted, then processed. Everything that must refuse a batch is checked
before the answer: the request signature, the key, the mandate and its caps, the
verified business, the batch seal, its Merkle root over the records, the certificate,
the AI policy version, seq, and the business's fair use. Then:
200 with
every outcome (PublishOutcome), as before;202 at once with
the push's status (PushStatus) and a Location header: poll
GET /v1/pushes/{push_id} until state is complete (or held, or failed). The
records are checked and published in chunks of 200 in the background; a notice tells
the business's owners and admins when it finishes.If background processing cannot be reached, a push of up to 500 records is processed
inside its request (200). A larger one is answered 503 with Retry-After: 60 and
retry_after_seconds: send the same batch again later; a batch that was already accepted
is kept and resumes.
For more than 10,000 records, upload a bulk file (POST /v1/bulk-uploads, then
POST /v1/bulk-imports).
A push that does not answer, or stops (a timeout, a dropped connection, a 5xx, a
failed status): every record of the batch has a row in GET /v1/push-attempts and on
its status — rejected with its reasons, or pending — and each pending row becomes
accepted, updated or held in the same commit that publishes (or holds) its record.
Sending the exact batch again (the same batch_seal and records, a new request
signature — accepted after the five-minute sealed_at window too, since it was accepted
before) resumes it: the records it already settled keep their outcome, the rest are
completed, and nothing is published twice. A push still being processed is only reported.
Any other batch under the same seq is refused (seq_not_increasing).
| type required | string (PathRecordType) Enum: "products" "business-files" "events" "jobs" "updates" Example: products The record type. Only |
| MDB-Sandbox-Key | string [ 1 .. 8192 ] characters Example: mdb_sbxk1.eyJ2IjoxLCJraW5kIjoicmV0cmlldmFsIn0.c2lnbmF0dXJl Sandbox only. The |
| Idempotency-Key required | string [ 1 .. 257 ] characters Example: 5f2b8c1e-7d3a-4e8f-9b6c-2a1d0e9f8c7b Required on every POST that creates something. 1–255 printable
ASCII characters. The same key with the same request replays the first answer; with a
different request it is |
required | object (BatchSealEnvelope) |
| records required | Array of strings [ 1 .. 10000 ] items [ items <= 204800 characters ] At most 10,000 records and 32 MiB of request body; 50 or fewer are answered inline (200), more are accepted and processed in the background (202). |
{- "batch_seal": {
- "payloadType": "application/vnd.masterdb.batch-seal.v2+json",
- "payload": "eyJ2IjoyLCJzZXEiOjQyfQ==",
- "signatures": [
- {
- "keyid": "0aWsmgFfrC73s0FnNjVRKhUR9B_jfREbJn8DnuxdT1g",
- "sig": "3q2+7w=="
}
]
}, - "records": [
- "eyJzY2hlbWEiOiJtYXN0ZXJkYi9wcm9kdWN0cy8xIiwiYnVzaW5lc3NfcHJvZHVjdF9pZCI6IlRSLTU1MjEifQ==",
- "eyJzY2hlbWEiOiJtYXN0ZXJkYi9wcm9kdWN0cy8xIiwiYnVzaW5lc3NfcHJvZHVjdF9pZCI6IlRSLTU1MjIifQ=="
]
}{- "batch_id": "bat_7d3a4e8f9b6c",
- "seq": 42,
- "held": false,
- "results": [
- {
- "leaf_index": 0,
- "outcome": "updated",
- "record_id": "mdb_dq5jnqatnemnqj4g3xmgzgpoik",
- "version": 7,
- "sha256": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
- "seal_digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}, - {
- "leaf_index": 1,
- "outcome": "rejected",
- "errors": [
- {
- "code": "money_not_string",
- "pointer": "/prices/0/amount",
- "detail": "amount is a decimal string, e.g. \"129.00\""
}
]
}
]
}Takes a record out of serving with a tiny sealed document {record_id, action: withdraw, at} signed by the business — never an HTTP verb on a bare id. The record of
truth is never removed and history stays; the published pointer moves and the fan-out
removes the rows and mirror documents from every region within seconds.
| type required | string (PathRecordType) Enum: "products" "business-files" "events" "jobs" "updates" Example: products The record type. Only |
| MDB-Sandbox-Key | string [ 1 .. 8192 ] characters Example: mdb_sbxk1.eyJ2IjoxLCJraW5kIjoicmV0cmlldmFsIn0.c2lnbmF0dXJl Sandbox only. The |
| Idempotency-Key required | string [ 1 .. 257 ] characters Example: 5f2b8c1e-7d3a-4e8f-9b6c-2a1d0e9f8c7b Required on every POST that creates something. 1–255 printable
ASCII characters. The same key with the same request replays the first answer; with a
different request it is |
| document_base64 required | string <base64> The exact bytes of |
required | object (SealEnvelope) A Path B seal (one record, sealed by a person's passkey). Format 2 ( |
{- "document_base64": "eyJyZWNvcmRfaWQiOiJtZGJfZHE1am5xYXRuZW1ucWo0ZzN4bWd6Z3BvaWsiLCJhY3Rpb24iOiJ3aXRoZHJhdyJ9",
- "seal": {
- "payloadType": "application/vnd.masterdb.seal.v2+json",
- "payload": "eyJ2IjoyfQ==",
- "signatures": [
- {
- "keyid": "0aWsmgFfrC73s0FnNjVRKhUR9B_jfREbJn8DnuxdT1g",
- "sig": "3q2+7w=="
}
]
}
}{- "record_id": "mdb_dq5jnqatnemnqj4g3xmgzgpoik",
- "action": "withdraw",
- "live_in": [ ]
}Deletes a record with a sealed document {record_id, action: delete, at}. Deletion
means deletion: the record leaves serving in seconds and its bytes are purged
from the master copy and backups within 30 days; only its fingerprints — the hashes
and log leaves — stay, which is what lets the public disavowal check still say when a
record with that hash was live.
| type required | string (PathRecordType) Enum: "products" "business-files" "events" "jobs" "updates" Example: products The record type. Only |
| MDB-Sandbox-Key | string [ 1 .. 8192 ] characters Example: mdb_sbxk1.eyJ2IjoxLCJraW5kIjoicmV0cmlldmFsIn0.c2lnbmF0dXJl Sandbox only. The |
| Idempotency-Key required | string [ 1 .. 257 ] characters Example: 5f2b8c1e-7d3a-4e8f-9b6c-2a1d0e9f8c7b Required on every POST that creates something. 1–255 printable
ASCII characters. The same key with the same request replays the first answer; with a
different request it is |
| document_base64 required | string <base64> The exact bytes of |
required | object (SealEnvelope) A Path B seal (one record, sealed by a person's passkey). Format 2 ( |
{- "document_base64": "eyJyZWNvcmRfaWQiOiJtZGJfZHE1am5xYXRuZW1ucWo0ZzN4bWd6Z3BvaWsiLCJhY3Rpb24iOiJkZWxldGUifQ==",
- "seal": {
- "payloadType": "application/vnd.masterdb.seal.v2+json",
- "payload": "eyJ2IjoyfQ==",
- "signatures": [
- {
- "keyid": "0aWsmgFfrC73s0FnNjVRKhUR9B_jfREbJn8DnuxdT1g",
- "sig": "3q2+7w=="
}
]
}
}{- "record_id": "mdb_dq5jnqatnemnqj4g3xmgzgpoik",
- "action": "withdraw",
- "live_in": [ ]
}The manifest of what your business has published — ids, your own business_product_id,
last_updated and source (manual, import or api) — in cursor pages, rate-limited above
1,000 records. It is how a connector reconciles its own state with MasterDB's; it is
not an export, and the only full-record path is the account export in the Business
Portal.
| cursor | string <= 512 characters The opaque |
| type | string (RecordType) Enum: "products" "business_files" "events" "jobs" "updates" Example: type=products The five published types, as collection names. |
| MDB-Sandbox-Key | string [ 1 .. 8192 ] characters Example: mdb_sbxk1.eyJ2IjoxLCJraW5kIjoicmV0cmlldmFsIn0.c2lnbmF0dXJl Sandbox only. The |
{- "entries": [
- {
- "record_id": "mdb_dq5jnqatnemnqj4g3xmgzgpoik",
- "business_product_id": "TR-5521",
- "type": "products",
- "last_updated": "2026-09-30T08:15:00.000Z",
- "source": "api"
}
], - "next_cursor": "eyJhZnRlciI6Im1kYl9kcTVqIn0="
}One of your own records as stored, by MasterDB's record id or by your own
business_product_id — the bytes you sealed, the seal, the sidecar and where it is
live. Only your own business's records are ever returned here.
| record_id required | string [ 1 .. 256 ] characters Example: mdb_dq5jnqatnemnqj4g3xmgzgpoik A MasterDB record id, or your |
| MDB-Sandbox-Key | string [ 1 .. 8192 ] characters Example: mdb_sbxk1.eyJ2IjoxLCJraW5kIjoicmV0cmlldmFsIn0.c2lnbmF0dXJl Sandbox only. The |
{- "record_id": "mdb_dq5jnqatnemnqj4g3xmgzgpoik",
- "business_product_id": "TR-5521",
- "type": "products",
- "record": {
- "schema": "masterdb/products/1",
- "business_product_id": "TR-5521",
- "product_name": "Ridge Trail Runner"
}, - "seal": {
- "payloadType": "application/vnd.masterdb.seal.v2+json",
- "payload": "eyJ2IjoyfQ==",
- "signatures": [
- {
- "keyid": "0aWsmgFfrC73s0FnNjVRKhUR9B_jfREbJn8DnuxdT1g",
- "sig": "3q2+7w=="
}
]
}, - "live_in": [
- "us-east4"
], - "last_updated": "2026-09-30T08:15:00.000Z",
- "source": "api"
}Every row a push tried to publish in a window, with its outcome and reason — a seal failure is its own category, distinct from a validation failure — so a rejected push is visible within seconds.
| from | string Example: from=2026-10-01T00:00:00Z Start of the window, inclusive (RFC 3339 UTC or a date). |
| to | string Example: to=2026-10-02T00:00:00Z End of the window, exclusive (RFC 3339 UTC or a date). |
| status | string Enum: "accepted" "updated" "rejected" "held" "pending" Example: status=rejected |
| cursor | string <= 512 characters The opaque |
| MDB-Sandbox-Key | string [ 1 .. 8192 ] characters Example: mdb_sbxk1.eyJ2IjoxLCJraW5kIjoicmV0cmlldmFsIn0.c2lnbmF0dXJl Sandbox only. The |
{- "attempts": [
- {
- "batch_id": "bat_7d3a4e8f9b6c",
- "leaf_index": 1,
- "received_at": "2026-09-30T08:15:00.000Z",
- "source": "api",
- "key_id": "0aWsmgFfrC73s0FnNjVRKhUR9B_jfREbJn8DnuxdT1g",
- "business_product_id": "TR-5521",
- "product_name": "Ridge Trail Runner",
- "outcome": "rejected",
- "reason": "money_not_string"
}
]
}A push or bulk import accepted then processed: its totals, its progress and a page
of its records' outcomes in leaf order (100 a page; next_cursor for the next). With
outcome, only the records with that outcome — e.g. rejected, to fix and send again.
Poll every poll_after_seconds while it is present. A push that has completed no step for
ten minutes carries stalled: true and a resume instruction (send the exact batch again —
a bulk import, the same import request again; what it already published is not published
twice). Signed with mdb-business-read: any live integration key of the business may read it.
| push_id required | string (PushId) ^bat_[0-9a-f]{24}$ Example: bat_7d3a4e8f9b6c1a2b3c4d5e6f A push (or bulk import) — the same id as its |
| outcome | string Enum: "accepted" "updated" "rejected" "held" "pending" Example: outcome=rejected |
| cursor | string <= 512 characters The opaque |
| MDB-Sandbox-Key | string [ 1 .. 8192 ] characters Example: mdb_sbxk1.eyJ2IjoxLCJraW5kIjoicmV0cmlldmFsIn0.c2lnbmF0dXJl Sandbox only. The |
{- "push_id": "bat_7d3a4e8f9b6c1a2b3c4d5e6f",
- "batch_id": "bat_7d3a4e8f9b6c1a2b3c4d5e6f",
- "type": "products",
- "seq": 42,
- "source": "api",
- "mode": "queued",
- "state": "processing",
- "held": false,
- "held_reasons": [ ],
- "records": 8000,
- "counts": {
- "rejected": 3,
- "pending": 3797,
- "accepted": 4100,
- "updated": 100,
- "held": 0
}, - "progress": {
- "stage": "commit",
- "chunk": 21,
- "chunks": 40
}, - "received_at": "2026-10-03T14:02:11.482Z",
- "updated_at": "2026-10-03T14:04:40.120Z",
- "completed_at": null,
- "error": null,
- "status_url": "/v1/pushes/bat_7d3a4e8f9b6c1a2b3c4d5e6f",
- "poll_after_seconds": 5,
- "results": [
- {
- "leaf_index": 17,
- "outcome": "rejected",
- "business_product_id": "TR-5521",
- "errors": [
- {
- "code": "money_not_string",
- "pointer": "/prices/0/amount",
- "detail": "amount is a decimal string, e.g. \"129.00\""
}
]
}
], - "next_cursor": "eyJhZnRlciI6IjE4In0"
}The bulk-file route for very large loads, step 1: a signed upload (valid one hour) for one file of up to 100,000 records and 256 MiB. The file
is newline-delimited: one record a line, each line the base64 of the record's exact
bytes (application/x-ndjson); line i is leaf i of the batch seal's Merkle tree.
POST the file to url as multipart/form-data with every member of fields, then the
file as file. The key needs a live mandate for the record type. Uploaded files are
deleted after 7 days, or as soon as their import completes.
Answered 503 with Retry-After (60 seconds) and reason: staging_unavailable where the
upload area cannot be written, and 503 with a longer Retry-After where bulk import is
not available.
| MDB-Sandbox-Key | string [ 1 .. 8192 ] characters Example: mdb_sbxk1.eyJ2IjoxLCJraW5kIjoicmV0cmlldmFsIn0.c2lnbmF0dXJl Sandbox only. The |
| Idempotency-Key required | string [ 1 .. 257 ] characters Example: 5f2b8c1e-7d3a-4e8f-9b6c-2a1d0e9f8c7b Required on every POST that creates something. 1–255 printable
ASCII characters. The same key with the same request replays the first answer; with a
different request it is |
| record_type | string (RecordType) Enum: "products" "business_files" "events" "jobs" "updates" The five published types, as collection names. |
{- "record_type": "products"
}{- "upload_id": "bup_5f1c0e9a8b7d6c5b4a3f2e1d",
- "record_type": "products",
- "fields": {
- "key": "bulk/0f3c9a/bup_5f1c0e9a8b7d6c5b4a3f2e1d.ndjson",
- "Content-Type": "application/x-ndjson",
- "policy": "eyJjb25kaXRpb25zIjpbXX0=",
- "x-algorithm": "SHA256"
}, - "content_type": "application/x-ndjson",
- "max_bytes": 268435456,
- "max_records": 100000,
- "expires_at": "2026-10-03T15:02:11.482Z"
}The bulk-file route, step 2: the uploaded file's upload_id and the batch seal over its
records — the same seal as a push's (tree_size the number of lines, root the Merkle
root over the records' raw bytes, a seq above the key's last). The seal, the key, the
mandate and its caps (over the file's bytes and records), the certificate, the AI policy
version and fair use are checked here; the file is then read once in the background, and
if its records are not exactly the ones the seal names the import is failed with
seal_invalid before any record is checked. Otherwise it is processed as a queued push:
poll GET /v1/pushes/{push_id}. The same request again answers the import's status, and
resumes one that stopped. Answered 503 with Retry-After where bulk import is not available
(the upload area cannot be written, reason: staging_unavailable), and where background
processing cannot be reached (the import is kept: send the same request again).
| MDB-Sandbox-Key | string [ 1 .. 8192 ] characters Example: mdb_sbxk1.eyJ2IjoxLCJraW5kIjoicmV0cmlldmFsIn0.c2lnbmF0dXJl Sandbox only. The |
| Idempotency-Key required | string [ 1 .. 257 ] characters Example: 5f2b8c1e-7d3a-4e8f-9b6c-2a1d0e9f8c7b Required on every POST that creates something. 1–255 printable
ASCII characters. The same key with the same request replays the first answer; with a
different request it is |
| upload_id required | string^bup_[0-9a-f]{24}$ |
required | object (BatchSealEnvelope) |
{- "upload_id": "bup_5f1c0e9a8b7d6c5b4a3f2e1d",
- "batch_seal": {
- "payloadType": "application/vnd.masterdb.batch-seal.v2+json",
- "payload": "eyJ2IjoyLCJzZXEiOjQzfQ==",
- "signatures": [
- {
- "keyid": "0aWsmgFfrC73s0FnNjVRKhUR9B_jfREbJn8DnuxdT1g",
- "sig": "3q2+7w=="
}
]
}
}{- "push_id": "bat_7d3a4e8f9b6c1a2b3c4d5e6f",
- "batch_id": "bat_7d3a4e8f9b6c1a2b3c4d5e6f",
- "type": "products",
- "seq": 42,
- "source": "bulk",
- "mode": "queued",
- "state": "queued",
- "held": false,
- "held_reasons": [ ],
- "records": 8000,
- "counts": {
- "rejected": 3,
- "pending": 3797,
- "accepted": 4100,
- "updated": 100,
- "held": 0
}, - "progress": {
- "stage": "split",
- "chunk": 21,
- "chunks": 40
}, - "received_at": "2026-10-03T14:02:11.482Z",
- "updated_at": "2026-10-03T14:04:40.120Z",
- "completed_at": null,
- "error": null,
- "status_url": "/v1/pushes/bat_7d3a4e8f9b6c1a2b3c4d5e6f",
- "poll_after_seconds": 5
}What the next seal must name: cert_id, the business's certificate
in force (a seal naming any other is refused), and ai_policy_version, the business's
own AI policy version in force — the live generation of its AI policy record, 0 before
one is sealed. A seal binds the version in force at sealed_at (seal format 2's
ai_policy_version; format 1 named terms_version); an AI policy record's own
seal names the version it creates, next_ai_policy_version. Before this read a pushing
system learned the version only from a refusal (seal_invalid with an
ai_policy_version extension). Any live integration key of the business may read it.
| MDB-Sandbox-Key | string [ 1 .. 8192 ] characters Example: mdb_sbxk1.eyJ2IjoxLCJraW5kIjoicmV0cmlldmFsIn0.c2lnbmF0dXJl Sandbox only. The |
{- "business_uuid": "0b7d6c3e-2f4a-4c1b-9d8e-7f6a5b4c3d2e",
- "cert_id": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
- "certificate_status": "active",
- "ai_policy_version": 3,
- "ai_policy_live_from": "2026-09-15T10:00:00.000Z",
- "ai_policy_record_id": "aip_abcdefghijklmnopqrstuv",
- "next_ai_policy_version": 4
}Your business's own figures per day, per country and per AI group (one name per group) — searches that returned your records, fetches, ad renders, clicks and spend — as at the end of the last complete hour. Never an impression figure MasterDB cannot know, never share of showings.
| from | string Example: from=2026-10-01T00:00:00Z Start of the window, inclusive (RFC 3339 UTC or a date). |
| to | string Example: to=2026-10-02T00:00:00Z End of the window, exclusive (RFC 3339 UTC or a date). |
{- "as_at": "2026-10-01T14:00:00.000Z",
- "days": [
- {
- "date": "2026-09-30",
- "country": "US",
- "ai_group": "Example AI",
- "searches": 1204,
- "fetches": 310,
- "ad_renders": 88,
- "ad_clicks": 4,
- "spend_gross": {
- "amount": "12.40",
- "currency": "USD"
}
}
]
}The AI groups that retrieved your business's records over the last 30 days, by name, with counts, country as the axis. It is what the business-side block decision is made from.
| country | string (Country) ^[A-Z]{2}$ Example: country=US ISO 3166-1 alpha-2, from the platform vocabulary. |
{- "as_at": "2026-10-01T14:00:00.000Z",
- "window_days": 30,
- "groups": [
- {
- "ai_group_id": "Qm3vT8kLp2XwZ9aB4cDe",
- "name": "Example AI",
- "country": "US",
- "searches": 18231,
- "fetches": 4410
}
]
}