Plank help · updated 2026-09-10

Working with egov.kz (the citizen portal)

How egov.kz actually works underneath: one JSON API, an ЭЦП login whose signed document is a constant, twenty-eight services that all order through the same three calls — and the one step that genuinely needs a human every time.

Agents: fetch the raw markdown of this page at /en/help/egov-portal.md

Working with egov.kz (the citizen portal)

egov.kz was rebuilt as a Next.js application over a single JSON API. That changes what is possible: the portal's own browser client does nothing you cannot do from a script, so there is no reason to drive egov.kz with a browser — see §8.

Everything below was read off the portal's own client bundle and verified against the live system on 2026-09-10 from a server outside Kazakhstan. There is no developer programme and no documentation, so this can change without notice: write code that fails loudly, not code that silently returns nothing.


1. The short version

  • NCALayer is not required, and this is proven against egov itself. On 2026-09-10 a login document signed by Kalkan under OpenJDK 17, with a real universal ГОСТ key and no NCALayer anywhere, was accepted: 200 with a session_id. See Signing with a Kazakhstan ЭЦП key.
  • The wall is not the signature. It is the second factor. After egov accepts the signature it sends an SMS (юрлицо) or a push into eGov Mobile (физлицо), and nothing proceeds without that code. Design for a human at login and never per action.
  • One key file covers both steps. The portal asks for two different EKU purposes, but since April 2024 НУЦ РК issues a single universal ГОСТ certificate that has to carry both — kz-ecp-signing §3. Only a customer still holding a pre-2024 pair hands over two files.
  • Twenty-eight services all order the same way, so one integration covers all of them.

2. The API

Base: https://fgw.egov.kz, with the route appended verbatim. Authenticated routes carry Authorization: Bearer <access_token>; send Origin: https://egov.kz.

Measured on 2026-09-10: reachable from outside Kazakhstan, no geo-block, no captcha, no CSRF token, and errors come back as typed codes with kk/ru/en messages:

{"code":"AUTH_ERROR_BAD_SIGNATURE","type":"ERROR",
 "message":{"ru":"ЭЦП недействительна. …","en":"The digital signature is invalid. …"}}

A typed code beats Tizilim's 422 «Ошибка при обработке сертификата», which is returned for every signing failure alike (kz-ecp-signing §6) — but do not expect it to point at your defect. Measured 2026-09-10: the literal string "not-a-signature" and a well-formed ГОСТ XMLDSig made with the ЭСФ SDK's expired 2019 test certificate both come back as AUTH_ERROR_BAD_SIGNATURE, byte for byte. The code names the class of problem, not the thing to change, so §6's one-variable-at-a-time discipline still applies here.

The catalog is public, and it needs no login

Three CMS routes answer with no token at all, which is how a service code turns into a name before you have a session:

GET /v1/cms/passports?size=100&page=<n>   → 940 published entries, 635 distinct codes
GET /v1/cms/passports/popular             → the ten on the front page, titles in kk/ru/en
GET /v1/cms/dictionaries/category-tree    → the tree, two roots: `fl` Гражданам, `ul` Бизнесу

Every item carries service_code, a title and a category_breadcrumb such as ul.development.regAndClose. Two traps, both measured 2026-09-10:

  • An unknown query parameter is ignored, not rejected. ?serviceCode=P3001 answers 200 with the ordinary unfiltered first page — and a filter that silently does nothing is indistinguishable from a service that legitimately has one match. size and page work; limit, offset, pageSize and perPage are all in the ignored class.
  • A wrong argument comes back as 401. by-category takes categoryId and nothing else: ?category_id=… and /by-category/<id> both answer 401, which reads as a permissions problem and is not one. It also accepts only the second level of the tree — a third-level id answers 404.

3. Logging in with ЭЦП — three steps, and the third needs a person

Step 1 — sign a constant

This portal has no "ask the server for the document" step. The XML is built by the client and never varies:

<?xml version="1.0" encoding="UTF-8"?><login><client-type>PORTAL</client-type></login>

That is the whole document. Sign it as enveloped XMLDSig with the auth key (AUTH_RSA…, EKU OID 1.3.6.1.5.5.7.3.2). The request the page hands NCALayer, verbatim from the bundle:

{
  "module": "kz.gov.pki.knca.basics",
  "method": "sign",
  "args": {
    "data": "<the login XML above>",
    "allowedStorages": ["PKCS12", "AKKaztokenStore", "AKKZIDCardStore"],
    "format": "xml",
    "signingParams": {},
    "signerParams": { "chain": undefined, "extKeyUsageOids": ["1.3.6.1.5.5.7.3.2"] },
    "locale": "ru"
  }
}

signingParams: {} means every default — not decoded, not encapsulated, not pre-hashed, no TSA. chain is populated only from NEXT_PUBLIC_PKI_ROOT_CHAIN_TEST, which is a test variable: in production the page pins no chain, so you do not need to supply the НУЦ РК roots either.

Note this is the general shape's exception. kz-ecp-signing §1 says the server builds the document so you never construct it yourself; that holds for ЭСФ and Tizilim and does not hold for the egov login. It does hold for every egov service — §4.

Step 2 — exchange the signature for a session

POST /identity/v3/auth/eds        {"certificate": "<the signed XML>"}
  → {"session_id": …, "is_ul": bool, "phone": "…"}

Step 3 — the second factor, and there is no way around it

Step 2 already sent it. Measured 2026-09-10: the SMS arrives from the /identity/v3/auth/eds call itself — nothing else has to be called, and there is no way to ask for a session without messaging the account holder. (POST /identity/v3/auth/otp/send, which takes the session_id, is the resend button, not the first send.) Plan around that: every login attempt costs the customer an SMS, so do not treat /auth/eds as a probe you can retry freely.

The client reads is_ul and branches: юрлицо → SMS, физлицо → push into eGov Mobile. Then:

POST /identity/v3/auth/verify     → {"access_token": …, "refresh_token": …}
POST /identity/v3/auth/token/refresh   (keeps it alive; both are JWTs with iat/exp)

Neither an SMS nor a push can be produced by the automation, so a person is in the loop at every login. Budget for it in the design rather than discovering it in front of a customer: hold the refresh token and keep the session alive, so the cost is one interaction per token lifetime and not one per action. For a юрлицо or ИП the code arrives by SMS to the registered phone, which is the easier of the two to arrange.


4. Ordering a service — three calls, and they are the same for all 28

POST /v1/<SVC>/xml                → {xml, request_number, uuid, applicant_uin}
      sign xml with the SIGNING key            (EKU OID 1.3.6.1.5.5.7.3.4)
POST /v1/<SVC>/signing/send-eds   {"signed_xml": …, "request_number": …}
GET  /v1/<SVC>/request-status

The requested purpose changes between login and submission. Login asks for EKU …3.2, a service application for EKU …3.4 — two visibly different calls in the same bundle. kz-ecp-signing §3 offered that split as an inference; on egov it is measured.

What it no longer implies is two files — and the reason is not the one you would guess.

A real universal ГОСТ certificate was read on 2026-09-10 (a ТОО's first-head key, issued June 2026). Its extended key usage is 1.3.6.1.5.5.7.3.4 plus the Kazakh-arc OIDs 1.2.398.3.3.4.3.2, 1.2.398.3.3.4.1.2 and 1.2.398.3.3.4.1.2.1. 1.3.6.1.5.5.7.3.2 — the OID this portal's login asks for — is absent. And the login was accepted anyway, 200 with a session_id.

So extKeyUsageOids is a key picker for the desktop, not a rule the server enforces: it tells NCALayer which of the files on someone's disk to offer, and the server checks the signature rather than the certificate's stated purpose. One universal key therefore serves both steps even though it does not claim clientAuth. Ask for the key — and, if the customer's is a surviving pre-2024 pair, for both halves of it.

Scope this honestly. It is measured on the login endpoint only. Whether /v1/<SVC>/signing/send-eds is as relaxed is untested, and Tizilim may well not be.

<SVC> is the state service code. Twenty-eight of them carry this exact triple; the public catalog in §2 gives twenty-four of those a name:

CodeServiceFiled under
P3001Справки/сведения по юридическим лицамБизнесу · Регистрация
P3041Справка о наличии недвижимости (Форма-6) для юридических лицБизнесу · Недвижимость
P110Выдача выписок из лицевого счета о состоянии расчетов с бюджетомГражданам · Налоги
P305Справка о правах на недвижимость (Форма-2)Гражданам · Недвижимость
P3061Справка о наличии недвижимости (Форма-6) для физических лицГражданам · Недвижимость
P601Получение справки о пенсионных отчисленияхГражданам · Пенсии
P605Сервис получения информации о назначении пособий и пенсионных выплатГражданам · Пенсии
P608Получение справки о подтверждении инвалидностиГражданам · Соцобеспечение
P631Выдача справки по назначению АСПГражданам · Соцобеспечение
P640Выплаты по случаю потери работыГражданам · Занятость
P641Назначение АСПГражданам · Соцобеспечение
P703Прикрепление к поликлиникеГражданам · Ребенок
P704Справки из наркологии, психиатрии и тубдиспансераГражданам · Медицина
P714Выдача справки о временной нетрудоспособностиГражданам · Медицина
P1001Справка о несудимости (электронная)Гражданам · Трудоустройство
P1002Сведения о совершении лицом административного правонарушенияГражданам · Право
P1005Выдача сведений о совершении лицом коррупционного преступленияГражданам · Трудоустройство
P1902Добровольный отказ от получения банковских займов, микрокредитовГражданам · Кредитная история
P3006Предоставление сведений о зарегистрированном юрлице на заданную датуГражданам · Архив
P4005Сведения о регистрации в приграничной территорииГражданам · Регистрация
P4006Выдача удостоверения личности РКГражданам · Удостоверение
P6132Выплаты на рождение ребенкаГражданам · Ребенок
P6504Информация о статусе стипендиата международной стипендии «Болашак»Гражданам · Образование
P8001Информация о начислениях ЕНПФГражданам · Пенсии

Twenty-six of the twenty-eight are citizen services. The portal files exactly two — P3001 and P3041 — under Бизнесу. That is the measurement; the judgement it invites is that the automatable surface of egov is mostly personal paperwork, and an accountant's working day is not in it. Read the filing loosely at the edges, though: P110 is a tax-account statement filed under Гражданам that an ИП needs as much as a person does.

Four route codes have no published passport — P2203, P3002, P3005, P3011. P3001 is a group rather than a single service (its own bundle calls it P3001_GROUP_SEARCH_ORGANIZATIONS), and its neighbours P3003, P3004, P3006, P3007, P3008, P3010, P3013 are all published on their own — so the likeliest reading is that the four unnamed ones are steps inside that group. That is an inference, not a measurement.

Nothing here can be prepared before the human has finished login. POST /v1/<SVC>/xml answers 401 without a bearer token (measured 2026-09-10), so there is no way to build the application first and collect the SMS afterwards — the person comes first, always.

A few services carry extra reads of their own (/children-info, /guardiansover-info-by-guardian, address dictionaries); REG02 and REG06 are registration flows and differ.

accepted is not done. Poll request-status — the same rule ЭСФ taught us, where a submission returns success and then processes to FAILED.


5. egov.kz is TWO portals, and the new API cannot see the old one

This is the thing to internalise before promising a customer anything about their existing cases.

NewLegacy
Frontegov.kz (Next.js)my.egov.kz (AngularJS on JBoss)
APIfgw.egov.kz/v1/…my.egov.kz/one-inbox/rest-v2/…, /person-profile/rest-v2/…
AuthAuthorization: Bearer from /identity/v3/auth/edsSSO cookie from idp.egov.kz
Request idUUID (01a08c40-510e-70a1-…)integer (764613146)

A case filed through the legacy cabinet is invisible to everything in this document. Measured 2026-09-10 on a real ТОО account with a live session: an application the customer could see in my.egov.kz as approved did not appear in the new portal's history at all, its numeric id returned 404 from /v1/<SVC>/request-status, and the new API's bearer token was refused with a JBoss 403 on my.egov.kz. Two id spaces, two sessions, no bridge.

So "check whether my application went through" is not answerable with a bearer token unless the application was itself filed through fgw. The legacy inbox lives behind one-inbox/rest-v2/search/inbox and one-inbox/rest-v2/requests-history/, and reaching it means the idp.egov.kz ЭЦП login (kz-ecp-signing §5) rather than the one in §3 — the server-supplied xmlToSign flow, which is what that page described before this one was written. Both still exist; they are not alternatives to each other, they are different systems.

The new portal's own history

POST /dp/v3/status-notifications   {"limit": 1-100, "offset": n}
GET  /dp/v3/count-unread-notifications

limit is required and omitting it is a 500, not a 400 (INVALID_ARGUMENT: Limit must be between 1 and 100, but was: 0) — an infrastructure-shaped error for a missing argument. Paging is honest: has_more flips correctly against offset, so you can trust it rather than fetching everything to count.

POST /v1/<SVC>/xml is not a local draft — it registers. Each call adds a CREATED / «Заявка создана» row to that feed and increments the unread badge, with no signature and no send-eds. Nothing is submitted, but the customer sees it. Do not treat the first of the three calls as a dry run.


6. Reads that need a session but no signature

Worth knowing, because they cost nothing and they are where an agent should look first:

GET  /identity/v3/auth/user/info        → the ЮЛ's name and BIN
GET  /v1/P3001/organizations?bin=…      → registry name record (no address)
     …&name=… also works; one of bin/name is required, 400 otherwise
POST /v1/dp/personal-data               → the signed-in PERSON's record

Two traps measured 2026-09-10. /v1/P3001/organizations returns the entity's names and nothing else — a legal address is only in the issued справка, so answering "what address is on file" requires actually ordering one. And /v1/dp/* returns the person, not the organisation, even inside an is_ul: true session: a first-head certificate carries both the IIN and the BIN, so which of the two a route means is a property of that route, never of the session.


7. The QR path — signing with no key at all

The portal offers a second way to sign, in which the key never leaves the customer's phone:

POST /v1/push-sign                                          → sends the request to eGov Mobile
wss://fgw.egov.kz/v1/websocket/sign?request_number=…&uuid=…  → notified when signed

The /v1/<SVC>/xml response already carries what this needs (uuid, qr_url, xml_for_sign).

This is the honest answer for a customer who will not hand over a .p12, and that is a reasonable position to hold. The agent assembles the whole application; the person approves it on their phone. It costs one tap per signed action, so say plainly that it removes the typing and not the approvals.


8. Do not drive this portal with a browser

The Cloudflare browser runs in a data centre; wss://127.0.0.1:13579 in that browser is its loopback, not the customer's. NCALayer is unreachable from it, and Live View does not help — Live View works for passwords and OTP because those are typed into the page, whereas an ЭЦП is handed over by a program on the person's own machine.

None of that matters for the flow above, which is plain HTTP end to end. But "open egov in the browser pane and log in with the ЭЦП in my files" is what a customer will actually try, so be precise about what happens: the page's «Войти по ЭЦП» button hangs on the socket connection. It does not error usefully. Signing in for them is possible — it is §3, done agent-side — it simply is not the browser doing it.

Carrying that session into the pane needs localStorage, not cookies. The portal persists its session with a zustand persist store in localStorage (XLDAPR, msg_data), so a cookie jar alone restores nothing. Playwright's storageState covers both, and restoreStorageState already replays per-origin localStorage through an init script beside the cookies (apps/api/src/browser/browser-driver.service.ts). What does not exist today is a way to write a storage_state from outside the browser: it is only ever captured from a live context. Until that lands, an agent-side login cannot be handed to the pane.

Whatever you do, never try to reach a signing device from the browser session.


9. Before you sign anything

kz-ecp-signing §7 governs the key itself, and all of it applies. One rule is sharper here than anywhere else: an egov application is a legally filed request to the state, submitted in the customer's name. Assemble it, show it, ask, and only then sign. Never sign a send-eds on your own initiative.


10. What is verified, and what is not

Verified live on 2026-09-10 — the base host, every route shape above, the constant login XML, both EKU OIDs, and that the API answers from outside Kazakhstan with no geo-block, captcha or CSRF token. /identity/v3/auth/eds was exercised twice: with a garbage string, and with a real enveloped ГОСТ XMLDSig of the login document produced by Kalkan on OpenJDK 17 from the ЭСФ SDK's public test key. Both return 400 AUTH_ERROR_BAD_SIGNATURE — so the transport, the request shape and the signer all work; what is untested is a signature the portal has any reason to accept.

Also verified 2026-09-10, with a real universal ГОСТ key — Kalkan's enveloped XMLDSig of the login document is accepted (200, session_id, is_ul: true, the account's phone echoed back), with no NCALayer and no browser. The certificate does not carry 1.3.6.1.5.5.7.3.2 and the login worked regardless (§4).

Also verified 2026-09-10, with no credentials at all — the public CMS catalog (§2): 940 published passports over 635 distinct codes, the two-root category tree, and names for 24 of the 28 automatable service codes (§4). POST /v1/<SVC>/xml answers 401 unauthenticated, so a service application cannot be built ahead of the login.

The whole login chain is verified as of 2026-09-10. /auth/verify returned 200 with a 264-character access_token and a 265-character refresh_token, redeemed from the SMS that /auth/eds itself sent. ЭЦП → Kalkan → session → SMS → tokens, twice, with no NCALayer and no browser anywhere in it. Authenticated reads then answered live: user/info, P3001/organizations, dp/personal-data, dp/v3/status-notifications and P3001/xml (§5, §6).

Not verified — any send-eds submission, and therefore whether it enforces the EKU that login ignores; the QR path beyond the fact that /xml hands back a qr_url; and everything on the legacy my.egov.kz side, which no session obtained here can reach.