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.

Base URL
production   https://api.maincompliance.com
development  https://dev2-api.maincompliance.com
Send a law file
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.

Keep tokens out of URLs. Query strings end up in proxy and browser logs. Always send the token in the header.

To rotate a token, ask us to issue a new one; both work during the overlap window so you can switch without downtime.

Authorization header
Authorization: Bearer mc_live_7f3c…
401 response
{
  "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.

201 created 200 duplicate 400 empty_payload 400 invalid_multipart 400 missing_file 400 invalid_id 401 unauthorized 404 not_found 413 payload_too_large 507 insufficient_storage 400 metadata_required 400 invalid_metadata 413 metadata_too_large 500 internal_error

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 shape
{
  "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.

Part order matters: 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 kind or type field: [{"kind": "title", …}, …].

Order is preserved within each kind, so what you sent can be reconstructed exactly.

Parts

metadatafield, JSONRequired

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.

filefile partRequired

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

AuthorizationstringRequired

Your bearer token: Bearer <token>.

X-Instrument-IdstringOptional

Alternative to the instrument_id query parameter below.

Query parameters

instrument_idstringRecommended

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.

kindenumOptional

text or metadata — what the file part is. Inferred from its content type when omitted, falling back to unknown rather than guessing.

Any other keystringOptional

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.
A mismatch is still accepted. The bytes are stored and you get 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

Metadata size1 MiB / 10,000 items

Serialised, counted after expansion across all kinds.

Large integersrejected

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.

Request
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"
meta.json — object form
{
  "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 }
  ]
}
201 Created
{
  "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

iduuidRequired

The id returned when the delivery was accepted.

Request
curl https://dev2-api.maincompliance.com/v1/laws/8f14e45f-ceea-4a1b-9f2c-1d3b4a5e6f70 \
  -H "Authorization: Bearer $MC_TOKEN"
200 OK
{
  "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.

Request
curl https://dev2-api.maincompliance.com/v1/health
200 OK
{ "ok": true }

The delivery object

Returned whenever a delivery is accepted or retrieved.

iduuid

Our identifier for this delivery. Keep it — it is how you ask about the file later.

statusenum

received · processing · processed · failed. Everything starts as received; processing happens on our side afterwards.

duplicateboolean

true when we already held these exact bytes from you. See Idempotency.

restoredboolean

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.

supplierstring

Which supplier the token identified. You cannot set this yourself.

kindenum

text · metadata · unknown — what the file part was taken to be, as declared or inferred from its content type.

instrument_idstring

The instrument you declared, or null.

label_checkenum

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.

label_notestring

What was compared and what was found, in plain language. Present for every verdict.

bytesinteger

Exactly how many bytes we stored. Compare it with your file size to confirm nothing was truncated.

sha256string

SHA-256 of the stored bytes, hex encoded. Compare it with your own hash for end-to-end integrity.

received_attimestamp

When we accepted it, in UTC (ISO 8601).

request_idstring

Identifier for this specific HTTP request. Quote it in support questions.

Verify integrity locally
# 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.

What counts as identical? The exact bytes, from the same supplier, declared for the same 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.

200 OK — already held
{
  "id": "8f14e45f-ceea-4a1b-9f2c-1d3b4a5e6f70",
  "status": "received",
  "duplicate": true,
  "received_at": "2026-09-02T07:14:22.481Z"
}

Limits

Maximum payload1 GiB

Per request. Larger than any single statute we have seen; split genuinely larger batches into several requests. Exceeding it returns 413 payload_too_large.

Request timeout15 minutes

Generous enough for slow links and large archives. Set your client timeout to match rather than something shorter.

Formatsany

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.

TransportHTTPS only

TLS is required. Send the delivery as multipart/form-data with a metadata field and a file part.

413 Payload Too Large
{
  "error": {
    "code": "payload_too_large",
    "message": "Payload exceeds the 1073741824 byte limit."
  }
}