Reference
Backend API
Every HTTP endpoint across the seven JAX-RS resources, with parameters, payloads and status codes.
Seven JAX-RS resources under /api, plus the WebAuthn ceremony endpoints the Quarkus extension mounts under /q/webauthn. Everything produces application/json.
Reached from the browser, every path below is prefixed /backend instead of /api, because of the Next.js rewrite. POST /api/files/1/action is POST /backend/files/1/action from the client. The /q/webauthn/* paths are not rewritten and keep their prefix.
Authentication
Callers are identified by the encrypted Quarkus WebAuthn session cookie. There are no API keys and no bearer tokens.
| Endpoint | Access |
|---|---|
POST /api/files/{id}/action |
authenticated; 401 if anonymous |
POST /api/approvals |
authenticated; 401 if anonymous |
GET /api/approvals |
authenticated; 401 if anonymous |
GET /api/audit/integrity |
auditor |
POST /api/devices/{id}/revoke |
administrator |
| all other reads | officer |
GET /api/session, POST /api/verify |
public |
Only two endpoints are public. GET /api/session has to be, because the workstation asks it before it knows whether anyone is signed in. POST /api/verify is public by design — anyone holding a signed document should be able to check it — and is rate limited rather than gated.
An unauthenticated call to anything else returns 401 with a JSON body. Note that the WebAuthn mechanism's own answer is a 302 to a login page, which would make fetch follow the redirect and receive HTML; ApiChallengeFilter rewrites that to a 401 for /api/* only.
Roles come from the role column on user_account. officer can read; approver can record decisions; auditor can verify the audit chain; administrator can revoke a device. A role implies the lesser ones, so @RolesAllowed("officer") admits an approver too.
GET /api/files
Lists file summaries. All three query parameters are optional and combine with AND.
| Parameter | Type | Behaviour |
|---|---|---|
status |
string | Case-insensitive exact match on status. Blank is ignored |
priority |
string | Case-insensitive exact match on priority. Blank is ignored |
q |
string | Case-insensitive substring match against fileNumber or subject |
Ordering is fixed and not client-controllable: priority rank first (MOST_IMMEDIATE → IMMEDIATE → ROUTINE → anything else), then initiatedAt descending. That is what puts the urgent files at the top of the inbox without the officer sorting anything.
GET /api/files?status=PENDING&priority=MOST_IMMEDIATE
Returns 200 with a JSON array of FileSummary:
| Field | Type | Notes |
|---|---|---|
id |
number | primary key, used in the detail and action paths |
fileNumber |
string | unique, e.g. ITE&C/RTGS/2026/0247 |
subject |
string | up to 500 characters |
department |
string | |
category |
string | Policy directive, Financial sanction, Tender acceptance, Revenue order |
priority |
string | MOST_IMMEDIATE, IMMEDIATE, ROUTINE |
status |
string | PENDING, APPROVED, REJECTED, RETURNED |
currentHolder |
string | |
initiatedBy |
string | |
initiatedAt |
ISO-8601 instant | |
amount |
string or null | pre-formatted, e.g. ₹842.7 Cr. Not a number |
goNumber |
string or null | G.O. number once issued |
noteCount |
number | count of notings on the file |
amount being a display string rather than a decimal means no arithmetic or currency conversion is possible server side. It is deliberate for the demo but would not survive a real financial integration.
There is no pagination. Filtering happens in Java rather than in SQL — GovFile.listAll() loads every row and then streams — and noteCount issues one COUNT query per row, so a listing costs one query plus one per file.
GET /api/files/{id}
| Status | Body |
|---|---|
200 |
FileDetail |
404 |
{"message":"File not found"} |
FileDetail is FileSummary plus two fields:
| Field | Type | Notes |
|---|---|---|
body |
string | the document of record, up to 4000 characters |
notes |
array of NoteView |
the noting sheet, ordered by noteNumber ascending |
NoteView:
| Field | Type | Notes |
|---|---|---|
noteNumber |
number | 1-based, contiguous per file |
authoredBy |
string | |
designation |
string | |
authoredAt |
ISO-8601 instant | |
noteType |
string | GREEN for a permanent note, YELLOW for a draft |
content |
string | up to 2000 characters |
signatureHash |
string or null | the SHA-256 digest, present only on signed notes |
signedAt |
ISO-8601 instant or null |
POST /api/files/{id}/action
The only write the workstation performs. Transactional.
{
"action": "APPROVE",
"note": "Sanction accorded for release of the Q2 instalment.",
"documentHash": "6f1c…64 hex characters…9b2e"
}
| Field | Required | Notes |
|---|---|---|
action |
yes | APPROVE, REJECT or RETURN. Trimmed and upper-cased before matching |
note |
on REJECT and RETURN |
Optional on APPROVE, where it defaults to Approved and digitally signed. |
documentHash |
on APPROVE |
Must match ^[0-9a-fA-F]{64}$. Lower-cased before storing |
Validation runs in this order, and the first failure returns:
| Status | Condition |
|---|---|
401 |
identity.isAnonymous() — {"message":"Security-key authentication is required"} |
404 |
no GovFile with that id |
400 |
action not one of the three verbs |
400 |
note blank on REJECT or RETURN — {"message":"A note is required for REJECT"} |
400 |
documentHash absent or not 64 hex characters on APPROVE |
200 |
the refreshed FileDetail |
Note the ordering consequence: the identity check precedes the existence check, so probing for valid file ids without a session returns 401 rather than 404.
What each action writes
All three append a note numbered max(noteNumber) + 1 for the file, authored by identity.getPrincipal().getName() with designation hardcoded to Approving Authority.
| Action | Note type | New status | Also writes |
|---|---|---|---|
APPROVE |
GREEN with signatureHash and signedAt |
APPROVED |
an Approval row, and a FILE_APPROVED audit event |
REJECT |
GREEN |
REJECTED |
a FILE_REJECTED audit event |
RETURN |
YELLOW |
RETURNED |
a FILE_RETURNED audit event |
The Approval row written on approve is what makes the digest findable later:
approval.username = actor;
approval.orderId = file.fileNumber; // the file number, not the numeric id
approval.documentHash = documentHash;
approval.assurance = "WebAuthn user verification";
orderId carrying the file number is what lets /api/verify report PKCS#7 signature block located for order ITE&C/RTGS/2026/0247.
There is no status guard. A file already APPROVED can be approved again, which appends another note, writes another Approval row and another audit event.
POST /api/verify
Resolves a SHA-256 digest against the approval table. Public, and rate limited to 20 calls per minute per caller — the limiter is in memory, so with more than one replica the allowance is per pod. An anonymous verification no longer writes an audit row at all: a read that anyone can make is not accountability, and recording it let any caller grow the ledger without bound.
{ "documentHash": "4f2a9c1e7b83d6045ae91cf3728b60d95e14a7c206fb3891d4e75c2a08b6f317" }
| Status | Condition |
|---|---|
400 |
hash absent or not 64 hex characters |
200 |
VerificationResult, with valid either true or false |
A digest with no matching Approval is not an error. It returns 200 with valid: false and a FAIL/SKIP step list. Only a malformed digest is a 400.
VerificationResult:
| Field | Type | Notes |
|---|---|---|
valid |
boolean | an Approval row exists for this digest |
documentHash |
string | the normalised lower-case digest |
steps |
array of CheckStep |
always six entries, in a fixed order |
signer |
SignerInfo or null |
null when valid is false |
signatureFormat |
string | constant PKCS#7 detached (CCA-SP) |
timestampToken |
string or null | null when valid is false |
verifiedAt |
ISO-8601 instant |
CheckStep is {name, status, detail} where status is PASS, FAIL or SKIP. The six checks:
- Document hash integrity
- Signature present
- Certificate chain to CCA Root (RCAI)
- OCSP revocation status
- RFC 3161 timestamp
- Key usage — digitalSignature
When valid is false, check 1 still passes (the digest is well formed), check 2 fails, and checks 3 to 6 are SKIP with Not evaluated.
SignerInfo is constructed, not parsed. subject and issuer are compile-time constants, serialNumber is hash.substring(0, 16).toUpperCase(), and the validity window is now - 200 days to now + 530 days. No certificate is decoded and no OCSP responder is contacted. Security model sets out exactly where that boundary falls.
Every call appends a SIGNATURE_VERIFIED audit row, with outcome SUCCESS or FAILURE and actor anonymous for unauthenticated callers. Verification is therefore a write as well as a read, and the endpoint carries no rate limit.
GET /api/audit
| Parameter | Type | Behaviour |
|---|---|---|
type |
string | exact, case-sensitive match on eventType. Blank is ignored |
limit |
integer | default 100, capped at 200. Values <= 0 fall back to the default |
Ordered occurredAt descending. Returns 200 with an array of AuditView: id, eventType, actor, actorRole, targetId, occurredAt, outcome, detail, correlationId.
limit sets a page size but there is no page index, so the endpoint returns the most recent limit rows and nothing older.
Event types actually emitted:
eventType |
Written by |
|---|---|
FILE_APPROVED |
FileResource.act() |
FILE_REJECTED |
FileResource.act() |
FILE_RETURNED |
FileResource.act() |
SIGNATURE_VERIFIED |
VerifyResource.verify() |
AUTH_SUCCESS |
seeded only |
CREDENTIAL_REGISTERED |
seeded only |
AUTH_SUCCESS and CREDENTIAL_REGISTERED appear in the seed data to show the shape of an authentication record; the ceremony endpoints belong to the Quarkus extension and do not call AuditEvent.record() themselves.
GET /api/devices
| Parameter | Type | Behaviour |
|---|---|---|
state |
string | upper-cased then matched exactly. Blank is ignored |
Ordered by serialNumber. Returns 200 with an array of DeviceView: id, serialNumber, icType, firmwareVersion, state, provisionedCa, assignedTo, certSubject, certSerial, validFrom, validTo, registeredAt.
Read-only. There is no endpoint to register, assign or revoke a device — the inventory is whatever the seeder wrote.
GET /api/dashboard
No parameters. Returns 200 with six counters:
| Field | Query |
|---|---|
pendingFiles |
GovFile where status = 'PENDING' |
approvedToday |
AuditEvent where eventType = 'FILE_APPROVED' and occurredAt >= startOfDay |
rejectedToday |
AuditEvent where eventType = 'FILE_REJECTED' and occurredAt >= startOfDay |
totalDevicesActive |
SigningDevice where state = 'ACTIVE' |
totalAuditEvents |
AuditEvent total |
avgApprovalMinutes |
mean minutes from file.initiatedAt to note.signedAt, over signed notes on APPROVED files |
startOfDay is Instant.now().truncatedTo(ChronoUnit.DAYS) — UTC midnight, not IST. For an Andhra Pradesh user the counter rolls over at 05:30 local time, so approvals made between midnight and 05:30 IST are attributed to the previous day.
avgApprovalMinutes loads every signed note on an approved file into memory and divides with integer arithmetic, so the result is whole minutes.
GET /api/session
No parameters, @PermitAll. Returns 200:
{ "authenticated": true, "username": "demo.user" }
username is null when anonymous. This is what the workstation polls to decide whether the decision controls are enabled, and it is the honest signal that a WebAuthn assertion was accepted.
Removed: /api/approvals
ApprovalResource has been deleted. It predated the eFile model and wrote a bare
Approval row with no noting, no status change and no audit event, so a digest recorded
through it was resolvable by /api/verify while being invisible to the file it belonged
to. Nothing in the workstation called it.
POST /api/files/{id}/action is the only write path, and it does all four things in one
transaction. The SDK's approveDocument() and listApprovals() were removed with it.
WebAuthn ceremony endpoints
Mounted by quarkus-security-webauthn, enabled by these two properties:
quarkus.webauthn.enable-registration-endpoint=true
quarkus.webauthn.enable-login-endpoint=true
| Path | Purpose |
|---|---|
/q/webauthn/webauthn.js |
the ceremony client that defines window.WebAuthn |
/q/webauthn/register-options-challenge |
registration challenge |
/q/webauthn/register |
registration completion; calls WebAuthnUserProvider.store() |
/q/webauthn/login-options-challenge |
assertion challenge |
/q/webauthn/login |
assertion completion; calls WebAuthnUserProvider.update() |
/q/webauthn/logout |
clears the session cookie |
The SDK never calls these directly. It delegates to new window.WebAuthn() so the ceremony client always matches the extension version that served it. The one exception is logout(), which fetches /q/webauthn/logout itself.
Health endpoints
quarkus-smallrye-health is on the classpath, so /q/health, /q/health/live and /q/health/ready are all served. ready reports datasource reachability, which makes it the useful one for a probe or a smoke check.
Error shape
Every resource declares its own ErrorResponse record, and all three are structurally identical:
{ "message": "A valid SHA-256 document hash is required to sign" }
There is no error code and no field-level detail, so a client distinguishes causes by status and reads the message for anything finer. There is no ExceptionMapper either, so an unhandled exception returns a Quarkus default 500 in a different shape.