# e-taxes.gov.az Cabinet Internals — Operational Notes Persistent notes for future Claude sessions about the ГНС cabinet (new.e-taxes.gov.az). Add to this file when you learn something non-obvious. Keep entries dated. --- ## High-level architecture (verified 2026-05-26) Two separate SPAs under one domain: | Path prefix | Mount point | Purpose | webpackJsonp name | |---|---|---|---| | `/etaxes/` | `
` | Public services (lookup taxpayer, debt query, etc.) | `webpackJsonpetaxes` | | `/eportal/` | `
` | **Cabinet for authenticated users — declarations live here** | (varies) | Both: React + Redux Toolkit + RTK Query, CRA-ish bundle layout (`static/js/N.HASH.chunk.js`). Source maps NOT exposed. **Pitfall:** the cabinet's mount point is `#app`, NOT `#root`. A snippet that walks fibers from `#root` will return `hasRoot: false`. --- ## Auth — how the cabinet checks login REST (Python `requests`) — **simple**: send `x-authorization: Bearer ` header. Works for every documented endpoint. No cookies needed, no session. Browser SPA — **NOT** simple. Just injecting `x-authorization` into outgoing requests via Playwright's `route` is **NOT enough**. The SPA has a route guard that checks **Redux state** for the auth token. If Redux is empty → redirect to `/eportal/login`, regardless of what's in HTTP headers. Verified storage usage (grepped eportal bundle): - `localStorage` — only `surveyModal` (popup tracking) and `aztax-lang`. **No auth token**. - `sessionStorage` — empty after fresh page load. - Cookies — **none set** by any cabinet endpoint (verified `Set-Cookie` headers: empty on all responses including `/chooseTaxpayer`). **Implication:** to drive the cabinet headlessly you must EITHER: 1. Login through the SPA UI (PIN1 push to phone), OR 2. Inject the token into the Redux store via `page.evaluate()` after the SPA mounts. Action type is dynamically constructed in Redux Toolkit — grep `*-api`, `Slice/` patterns to find it. Untested as of 2026-05-26. Easier in practice: UI login + Playwright's `storage_state` to persist cookies+localStorage between runs (note: storage_state is mostly empty here, so reuse value is limited — the actual auth lives in in-memory Redux, which Playwright cannot serialize). ### REST ASAN flow (works without browser) ``` POST /api/po/auth/public/v1/asanImza/start body={"phone": "+994...", "userId": "..."} → returns 200 with bearer_token in x-authorization response header → response body: {"verificationCode": "1234"} (shown on user's phone screen) POLL GET /api/po/auth/public/v1/asanImza/status headers={x-authorization: Bearer } → returns {"successful": false, "error": null} while waiting → when user approves PIN1, eventually returns successful: true (or just becomes silently true) → BETTER: poll /asanImza/certificates instead — when it returns 200 with non-empty list, you're authed GET /api/po/auth/public/v1/asanImza/certificates → returns [{taxpayerType: "individual"|"legal", legalInfo|individualInfo, ...}, ...] POST /api/po/auth/public/v1/asanImza/chooseTaxpayer body={"ownerType": "legal", "legalTin": "VOEN"} → returns 200 with main_token in x-authorization response header → main_token JWT validity: 10 min (exp = iat + 600s) ``` **Pitfall — polling format:** my first explorer.py polled `data.get("status")` but the actual field is `data.get("successful")` (bool). Wrong field name → silent infinite loop. Now using "did certificates endpoint return non-empty data?" as success signal — more robust. **Pitfall — ГНС polling latency:** user approves PIN1 quickly, but ГНС server takes several seconds (5-30s observed) to flip `successful: true`. Use timeout >= 90s with 2s interval. ### Renewing tokens `RENEW_URL = "https://new.e-taxes.gov.az/api/po/auth/public/v1/renew"` — used by `taxes_az/auth.py:renew_token`. Returns 401 if token expired without recent activity. There's an `ACTIVITY_TIMEOUT_MINUTES = 10` gate in the project code. --- ## Login form (UI flow, /eportal/az/login) Initial page: `/eportal/az/login` — shows 5 method tiles. Click "Asan İmza" link (`` with text "Asan İmza", class `login-item-link`) → SPA navigates to `/eportal/az/login/asan` with form. ASAN form selectors (verified 2026-05-26): | Selector | Field | Notes | |---|---|---| | `#phone` | Phone (type=text) | placeholder `00 000 00 00` — enter LOCAL format only (e.g. `558239674`), **without** +994 | | `#userId` | User ID (type=password) | The Asan İmza user ID, e.g. `171082` | | `#loginPageSignInButton` | Submit (type=submit) | Text: `Daxil ol` | Page also has a language `