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:
200with asession_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=P3001answers200with the ordinary unfiltered first page — and a filter that silently does nothing is indistinguishable from a service that legitimately has one match.sizeandpagework;limit,offset,pageSizeandperPageare all in the ignored class. - A wrong argument comes back as
401.by-categorytakescategoryIdand nothing else:?category_id=…and/by-category/<id>both answer401, which reads as a permissions problem and is not one. It also accepts only the second level of the tree — a third-level id answers404.
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:
| Code | Service | Filed 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.
| New | Legacy | |
|---|---|---|
| Front | egov.kz (Next.js) | my.egov.kz (AngularJS on JBoss) |
| API | fgw.egov.kz/v1/… | my.egov.kz/one-inbox/rest-v2/…, /person-profile/rest-v2/… |
| Auth | Authorization: Bearer from /identity/v3/auth/eds | SSO cookie from idp.egov.kz |
| Request id | UUID (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.