Law Intake API
Push legislative material to MainCompliance in whatever format you already produce. Every law arrives with the metadata that describes it; we store your bytes exactly as sent and process them later.
This API has one job: accept and preserve. The law itself is never parsed, validated or transformed, so a delivery never fails because of its structure. XML, JSON, PDF, ZIP, HTML, plain text — all are accepted and stored verbatim, together with a JSON document describing what they are.
Every accepted payload is written to durable storage, fingerprinted with SHA-256, and recorded with the time we received it, the supplier it came from, and the filename you declared. You get an identifier back that you can use to confirm receipt.
Base URL
There are two environments, each its own host over HTTPS: production at api.maincompliance.com and development at dev2-api.maincompliance.com. They are separate systems with separate storage, and each issues its own token — a token from one is refused by the other. Deliver the same material to both. Plain HTTP is redirected and never carries a token.
production https://api.maincompliance.com development https://dev2-api.maincompliance.com
curl -X POST "https://dev2-api.maincompliance.com/v1/laws?instrument_id=SE_SFS_1999_1079" \ -H "Authorization: Bearer $MC_TOKEN" \ -F "metadata=<meta.json;type=application/json" \ -F "file=@1999_1079.xml;type=application/xml"
Authentication
Authenticate every request with the bearer token issued to you, in the Authorization header. Tokens identify which supplier a delivery came from, so keep yours private — anyone holding it can submit on your behalf.
Requests without a valid token receive 401 unauthorized. Nothing is stored and no detail about why the token failed is returned.
To rotate a token, ask us to issue a new one; both work during the overlap window so you can switch without downtime.
Authorization: Bearer mc_live_7f3c…
{ "error": { "code": "unauthorized", "message": "Missing or invalid bearer token." }, "request_id": "5c1f…" }
Errors
Errors use conventional HTTP status codes and always return the same JSON shape, so you can branch on error.code rather than parsing prose.
Every response — success or failure — carries an X-Request-Id header, repeated in the body. Quote it when reporting a problem and we can find the exact request in our logs.
Retrying
500, 507 and network failures are safe to retry — see Idempotency. Back off for a few minutes on 507: it means we are temporarily short of storage, and the condition is ours to clear, not yours. 401 and 413 will fail identically on retry; fix the token or the file size instead.
{ "error": { "code": "payload_too_large", "message": "Payload exceeds the 1073741824 byte limit." }, "request_id": "9a2e…" }
Upload a delivery
POST /v1/laws
Send the law and the metadata that describes it as one multipart/form-data request: a metadata field, then a file part. Both are required — a law without its metadata is an incomplete delivery, so there is no second way in.
metadata first, file last.
Send metadata as a plain field, not as a file upload. This lets us validate it before
writing a single byte of the law, and makes a mis-shaped request fail loudly rather than
storing the wrong bytes. A part after file, or a file-shaped part not named
file, is rejected with 400.
We never transform the law. Whatever you send in the file part is stored byte for byte, and the SHA-256 we return is the hash of exactly those bytes — the same value you get from shasum -a 256 locally. We do read the start of it, for one purpose: to confirm it is the law you said it was — see Label check.
Metadata: two accepted shapes
Send whichever matches what you already produce. Both are stored identically, so you do not need to reshape your data for us.
- Object — each key is a kind and its value expands to one item per element:
{"legislation": {…}, "docForm": [ … ], "relations": [ … ]}. The natural fit for a per-instrument delivery. - Array — each element is one item, named by its own
kindortypefield:[{"kind": "title", …}, …].
Order is preserved within each kind, so what you sent can be reconstructed exactly.
Parts
The document describing this law. Must be a valid UTF-8 JSON object or array, sent as an ordinary form field before the file. The exact metadata bytes are retained alongside the normalized items for each accepted revision, including repeated file deliveries.
The law itself, in any format — XML, JSON, PDF, ZIP, HTML, plain text. Must be the last part. Its filename and Content-Type are recorded as you declare them.
Headers
Your bearer token: Bearer <token>.
Alternative to the instrument_id query parameter below.
Query parameters
Which law this delivery is about, e.g. SE_SFS_1999_1079. It forms part of the delivery's identity, so the same bytes sent for a different instrument are a separate delivery rather than a confusing duplicate.
text or metadata — what the file part is. Inferred from its content type when omitted, falling back to unknown rather than guessing.
Kept alongside the delivery as request context — for example ?batch=2026-09-02. We do not interpret these; they travel with the file for whoever processes it.
Corrections
Re-sending the same law with corrected metadata records a new revision rather than discarding it. The response returns metadata_revision; the newest is current and earlier ones are kept as history. The kind recorded on first receipt is the one kept, and the response always shows what is stored.
Label check
Every delivery is checked for one thing: does the file agree with the label it arrived with? For formats we recognise we read the identifier the document carries for itself — the (2004:298) in an SFS title, the instrument_id on a placeholder stub — and compare it with the instrument_id you declared and the _idext_document in your metadata. The result comes back as label_check with a plain-language label_note.
ok— everything that could be compared agrees.mismatch— the file is a different law from the one declared, or the metadata describes another instrument. The note names both sides, e.g. payload is SE_SFS_2018_10 by its SFS title; delivery declared SE_FFFS_2018_10.unchecked— the file is a format we do not read (a PDF, say), so its label could not be confirmed. Not wrong, only unverified.
201 as usual, but nothing will process that delivery. Treat a mismatch in the response as a failed upload on your side: find the right file and send it. Re-sending the same wrong bytes only returns the same row.
Limits
Serialised, counted after expansion across all kinds.
Values beyond 253 cannot be represented exactly in JSON, so rather than store a silently rounded identifier we refuse the document. Send such values as strings.
Returns
201 with the delivery object when the bytes are new; 200 with duplicate: true when we already hold these exact bytes for this instrument.
curl -X POST "https://dev2-api.maincompliance.com/v1/laws?instrument_id=SE_SFS_1999_1079" \ -H "Authorization: Bearer $MC_TOKEN" \ -F "metadata=<meta.json;type=application/json" \ -F "file=@1999_1079.xml;type=application/xml"
// Node 20+ — FormData preserves insertion order, so append metadata first. import { openAsBlob } from 'node:fs'; import { readFile } from 'node:fs/promises'; const form = new FormData(); form.append('metadata', await readFile('meta.json', 'utf8')); form.append('file', await openAsBlob('1999_1079.xml'), '1999_1079.xml'); const res = await fetch( 'https://dev2-api.maincompliance.com/v1/laws?instrument_id=SE_SFS_1999_1079', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MC_TOKEN}` }, body: form }); console.log(await res.json());
import os, requests with open('meta.json') as m, \ open('1999_1079.xml', 'rb') as f: r = requests.post( 'https://dev2-api.maincompliance.com/v1/laws', params={'instrument_id': 'SE_SFS_1999_1079'}, headers={'Authorization': f'Bearer {os.environ["MC_TOKEN"]}'}, # metadata as a plain field, emitted before the file data={'metadata': m.read()}, files={'file': ('1999_1079.xml', f, 'application/xml')}, timeout=900, ) print(r.json())
{ "legislation": { "_idext_document": "SE_SFS_1999_1079", "_local_identifier": "SFS 1999:1079", "_year": 1999 }, "docLang": [ { "_title": "Revisionslag (1999:1079)", "_language": "sv" } ], "relations": [ { "_target_idext_document": "SE_SFS_2004_978", "_id_document_xref_type": 2 } ] }
{ "id": "8f14e45f-ceea-4a1b-9f2c-1d3b4a5e6f70", "status": "received", "duplicate": false, "supplier": "interjust", "kind": "text", "instrument_id": "SE_SFS_1999_1079", "label_check": "ok", "label_note": "payload SFS title and metadata agree on SE_SFS_1999_1079", "bytes": 84213, "sha256": "3b7f2c…", "metadata_items": 4, "metadata_revision": 1, "received_at": "2026-09-02T07:14:22.481Z" }
Retrieve a delivery
GET /v1/laws/{id}
Confirm that a delivery arrived and see how far it has got. Use it to reconcile a batch after sending, or to check whether something you pushed has been processed yet.
You can only read your own deliveries. An id belonging to another supplier returns 404, the same as an id that does not exist.
Path parameters
The id returned when the delivery was accepted.
curl https://dev2-api.maincompliance.com/v1/laws/8f14e45f-ceea-4a1b-9f2c-1d3b4a5e6f70 \ -H "Authorization: Bearer $MC_TOKEN"
{ "id": "8f14e45f-ceea-4a1b-9f2c-1d3b4a5e6f70", "status": "received", "supplier": "interjust", "kind": "text", "instrument_id": "SE_SFS_1999_1079", "label_check": "ok", "label_note": "payload SFS title and metadata agree on SE_SFS_1999_1079", "filename": "1999_1079.xml", "content_type": "application/xml", "bytes": 84213, "sha256": "3b7f2c…", "received_at": "2026-09-02T07:14:22.481Z" }
Health
GET /v1/health
Unauthenticated liveness check that also confirms the database is reachable. Use it to verify connectivity before starting a large batch.
Returns 200 with {"ok": true} when the service can serve uploads, and 503 when it cannot.
curl https://dev2-api.maincompliance.com/v1/health
{ "ok": true }
The delivery object
Returned whenever a delivery is accepted or retrieved.
Our identifier for this delivery. Keep it — it is how you ask about the file later.
received · processing · processed · failed. Everything starts as received; processing happens on our side afterwards.
true when we already held these exact bytes from you. See Idempotency.
Present on duplicates. true means we already had the record but no longer had the bytes, and your re-delivery put them back — a good reason to keep re-sending when in doubt.
Which supplier the token identified. You cannot set this yourself.
text · metadata · unknown — what the file part was taken to be, as declared or inferred from its content type.
The instrument you declared, or null.
ok · mismatch · unchecked. Whether the file agrees with the instrument it was declared as — see Label check. A mismatch is stored but will not be processed.
What was compared and what was found, in plain language. Present for every verdict.
Exactly how many bytes we stored. Compare it with your file size to confirm nothing was truncated.
SHA-256 of the stored bytes, hex encoded. Compare it with your own hash for end-to-end integrity.
When we accepted it, in UTC (ISO 8601).
Identifier for this specific HTTP request. Quote it in support questions.
# your hash should equal the one we return shasum -a 256 1999_1079.xml
Idempotency
Re-sending a file is free and safe. We fingerprint every payload, so if you send bytes we already hold from you for the same instrument, we keep the original and return it with duplicate: true and status 200 instead of 201.
That means you never need to track what you have already sent. If a connection drops or you are unsure whether a delivery landed, simply send it again — you will get back the original id, and no second copy is stored.
instrument_id. Change one character and it is a new delivery, which is what you want when a statute is amended — and the same bytes sent for a different instrument are also a new delivery, so metadata is never attached to the wrong law. If you omit instrument_id, identity falls back to bytes plus supplier.
Re-sending also repairs. If our record survived but the stored file did not, the duplicate response comes back with restored: true — your copy became the copy we keep. Sending again is never wasted.
{ "id": "8f14e45f-ceea-4a1b-9f2c-1d3b4a5e6f70", "status": "received", "duplicate": true, "received_at": "2026-09-02T07:14:22.481Z" }
Limits
Per request. Larger than any single statute we have seen; split genuinely larger batches into several requests. Exceeding it returns 413 payload_too_large.
Generous enough for slow links and large archives. Set your client timeout to match rather than something shorter.
We never inspect or reject on the law's content. XML, JSON, PDF, ZIP, HTML, plain text — send what you have. The metadata that accompanies it must be JSON.
TLS is required. Send the delivery as multipart/form-data with a metadata field and a file part.
{ "error": { "code": "payload_too_large", "message": "Payload exceeds the 1073741824 byte limit." } }