Government of Andhra Pradesh / MobileSigner eFile platform
Reference

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_IMMEDIATEIMMEDIATEROUTINE → 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:

  1. Document hash integrity
  2. Signature present
  3. Certificate chain to CCA Root (RCAI)
  4. OCSP revocation status
  5. RFC 3161 timestamp
  6. 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.