API — for integrators
*O'zbekcha: README.md*
Sign with a qualified electronic signature (ERI) on the Muhrdor server from your own system - a script, an ERP, an EDO - one document or a batch of up to 200. The key never leaves the HSM and is never given to you: your service account signs within a grant act the key owner signed, for the sites and document kinds the act names. Every signature is in the key owner's audit journal, marked "API".
- Machine-readable contract:
GET https://muhrdor.uz/api/v1/openapi.json(in the repository:docs/api/openapi-integrators.json) - Full reference (every endpoint and field, Uzbek): reference.md
- Error catalogue: errors.md
- A ready example (Python standard library only):
examples/muhrdor_sign.py - Wire format in detail (Uzbek):
docs/protocols/api-v1-service-account.md
1. Setup (once)
- Service account. In the Muhrdor panel (an ERI-attested account) → "API kalitlari" → a name → "Yaratish". You get a
client_id(sa_…) and aclient_secret- the secret is shown once, in that dialog. Keep it in your script's configuration (an environment variable), not in code. - Act. Send the
client_idto the key owner (for instance your client company's director). In the panel they choose "Huquqlar → Huquq berish → Xizmat akkaunti (client_id)" → "Topish" (they see your account's name and your name) → site(s) and document kinds → period → "Aktni imzolash". A service account never gets "all sites": each site is named in the act. - Check:
GET /api/v1/client/credentialslists the key withaccess: "grant"and the act's scope.
2. Quick start
export MUHRDOR_CLIENT_ID=sa_... export MUHRDOR_CLIENT_SECRET=... python examples/muhrdor_sign.py --site my.soliq.uz --credential <credential_id> report.xml # -> report.xml.p7b and "report.xml: <audit_ref>"
With curl:
TOKEN=$(curl -s https://muhrdor.uz/api/v1/token -H 'Content-Type: application/json' \
-d "{\"grant_type\":\"client_credentials\",\"client_id\":\"$MUHRDOR_CLIENT_ID\",\"client_secret\":\"$MUHRDOR_CLIENT_SECRET\"}" \
| jq -r .access_token)
curl -s https://muhrdor.uz/api/v1/client/credentials -H "Authorization: Bearer $TOKEN"
curl -s https://muhrdor.uz/api/v1/signatures/sign -H "Authorization: Bearer $TOKEN" \
-H 'Idempotency-Key: report-2026-09-0001' -H 'Content-Type: application/json' \
-d '{"credential_id":"…","site":"my.soliq.uz","documents":[{"content_b64":"…","external_ref":"report-9"}]}'3. Steps
- Token —
POST /api/v1/token{grant_type: "client_credentials", client_id, client_secret}→access_token, valid one hour. No refresh token: on 401 get a new one. Do not fetch a token per request (30 an hour per client). - Keys —
GET /api/v1/client/credentials:credential_idand the act's scope. - Sign —
POST /api/v1/signatures/sign:site— the site the documents are for (the act must name it);documents[]— 1..200, 5 MiB in all;content_b64,detached(defaulttrue- no content inside the PKCS#7;false- content embedded),external_ref— your own document id (≤64,A-Z a-z 0-9 . _ : / -), echoed back and kept in the journal;Idempotency-Keyis required — a new key for each new request (a UUID, say). Answer:results[i]—pkcs7_b64(DER PKCS#7, the E-IMZO shape),audit_ref,doc_class,external_ref; a refused document haserrorinstead.
- The result later —
GET /api/v1/signatures/{audit_ref}?credential_id=<key>: the signature parts and the PKCS#7 hash (the PKCS#7 itself is not stored on the server - keep your.p7b).
4. Retrying without signing twice
The connection dropped and no answer came: send again with the same Idempotency-Key and the same body. Within ten minutes the server returns the first answer; nothing is signed twice.
409 request_in_progress— the first one is still running: retry in a second with the same key.422 idempotency_mismatch— the key was used for another body: a new request needs a new key.429 too_many_requests— waitRetry-Afterseconds.502 signing_failedwithretryable: true— retry once with the same key.403 policy_denied— do not retry: the act does not cover it (reasonsays why).
5. Sites and document kinds
The document kind (doc_class) is recognised from the content. Kinds the act names in doc_kinds pass.
| Site | Kinds |
|---|---|
my.soliq.uz | hisobot (report), login |
didox.uz | factura (invoice), qabul (acceptance), hujjat (document), login |
xt-xarid.uz | hash32 |
my.it-park.uz | login |
Other XML/JSON is a structural kind (xml:<root element>, json:<digest>): it passes when the key owner names it in the act. Unrecognised content is UNCLASSIFIED_CONTENT. To learn which kind a document is: the refusal's detail and the key owner's panel ("Rad etilgan so'rovlar") show it, and the owner adds it to the act. A new site needs a Muhrdor adapter - write to us.
6. Limits
| What | Limit |
|---|---|
| Token | one hour; 30 an hour per client_id |
| One request | 200 documents, 5 MiB in all |
| Signing rate | 600 documents a minute per service account (a server setting) |
| Idempotency window | ten minutes |
| Active service accounts | ten per person |
7. Privacy and the journal
Document content is not stored. The journal keeps: hashes of the content and of the signed attributes, the document kind, the site, page_url (without the query), the document's number and period (from the site's field map, no amounts), your external_ref, the act reference and client: "api". Do not put personal data (a JSHSHIR, a name) in external_ref - it is an id.
8. Security
- The secret is visible only when the account is created or on "Secretni yangilash"; if it is lost, rotate it (the old secret and its tokens die at once).
- An account no longer needed: "O'chirish" (final). The key owner may revoke the act at any time - the next request is refused.
- An API token opens none of the person's endpoints: only the token,
client/*andsignatures/*.