Signed actions
A signed action is the only way to change a record on the DigiFIR ledger. A client builds a small JSON envelope, signs its canonical bytes with the person's device key, and posts it to the backend, which relays it to the chaincode that owns the record. The chaincode overview describes the envelope's fields; this page covers the client's side.
Before you start
- A session for the signer, opened with the same device key (see authentication).
- The signer's key ID (
key:<32 hex>) or, for a citizen,<did>#k<n>. - Every attachment already uploaded and verified through
POST /v1/files(see Events, files and verification).
Steps
Choose a new UUIDv7 as the action's
id. Keep it: it makes the action idempotent, and it becomes the ID of any record the action creates.Put the type's public fields in
body. Personal details and free text go into the private payload instead, as named sections, each with a randomsalt.Encode the private payload as canonical JSON (RFC 8785, Unicode NFC) and put
sha256:plus its SHA-256 hex inprivateHash.Encode the action as canonical JSON and sign those exact bytes with ES256. The signature is the 64-byte
r||svalue in base64url.Send the request:
httpPOST /v1/actions Authorization: Bearer <access token> X-Digifir-App: police/4.0.0 X-Digifir-Install: <installation id> Content-Type: application/json { "action": { "v": 1, "type": "fir.register", "id": "0193f2c4-...", "actor": "key:9f2c...", "org": "PoliceStateAMSP", "at": 1761631327123, "subject": "<caseId>", "body": { } }, "signature": "<base64url r||s>", "keyId": "key:9f2c...", "private": { } }Read the answer:
Status Meaning 200with anActionReceiptCommitted. The receipt has the action ID and type, txId,block, the transaction and commit times, the institutions that endorsed it, the submitting gateway, and the chaincode event.202with anActionPendingStill in progress ( RECEIVED,ENDORSING,SUBMITTEDorCOMMITTING). FollowstatusUrl, or wait foraction.committedoraction.failedon themeevent channel.409withretryableA read conflict that did not resolve after three tries. Keep the action and send it again later. 503withretryableAn institution's peers or the ledger are unreachable. Keep the action and send it again later. Any other 4xxRefused. The problem's codesays why, for exampleERR_ROLEorERR_STATE.
TIP
The SDKs do all of this, including the canonical encoding, signing with the platform key store, the encrypted offline outbox and the retries. Build actions with the ActionBuilder of the Dart SDK or its equivalent in the TypeScript SDK.
CAUTION
Never re-serialise an action after signing it. The backend and the chaincode check the exact bytes that were signed, and a re-encoded object with a different member order or number format fails as ERR_NOT_CANONICAL or ERR_BAD_SIGNATURE.
A repeated action with the same ID and bytes returns the original receipt with replayed: true. A different action that reuses an ID gets 409 CONFLICT.
Bundles
Some steps must commit in a fixed order. POST /v1/action-bundles takes up to five actions and submits each only after the one before has committed:
evidence.seize, thenevidence.memo;fir.register, thenhint.refer(register and send the pair to a magistrate);fir.handover, thenevidence.handover_keys, thenevidence.rehome(a cross-state handover).
A bundle stops at the first refusal. If it is interrupted, sending the same bundle ID again resumes it.
Signed reads
Reading private data uses the same signing, with type read.private and a body naming the chaincode, the purpose and up to 50 targets with the parts wanted. Send it to POST /v1/private-reads or /v1/private-reads/batch. A read request may be at most 120 seconds old, writes nothing to the ledger, and may be signed by a session-only key.
Action catalogue
Every action type, grouped by the chaincode that runs it. The roles page says who may sign each one.
| Chaincode | Action types |
|---|---|
identity-registry | key.register, key.revoke, did.register, did.add_key, did.revoke_key, did.link, ref.district, ref.station, ref.station_area, ref.court, ref.lab, jurisdiction.delegate, jurisdiction.revoke_delegation, config.propose, config.approve |
fir-registry | complaint.file, complaint.intake, complaint.add_info, complaint.sign, complaint.ask, complaint.enquiry_request, complaint.enquiry_decide, complaint.enquiry_close, complaint.ncr, fir.register, fir.register_zero, fir.handover, fir.assign_io, case.diary, case.statement, case.arrest, case.custody_event, fir.final_report, doc.register, hint.attest, hint.refer, clock.escalate, external.anchor, external.release, erasure.execute |
evidence-custody | evidence.seize, evidence.memo, custody.offer, custody.accept, custody.cancel, lab.dispatch, lab.accept, lab.refuse, lab.stage, lab.notes, lab.seal, evidence.escrow_keys, evidence.handover_keys, evidence.rehome, erasure.execute |
judicial-actions | court.referral_decide, court.closure_decide, court.cognizance, court.remand, court.bail, court.quash_record, court.transfer, court.status_request, police.status_reply, court.dispose, court.erasure_order, court.notes, erasure.execute |
These are the 66 types the relay's contract lists today. Chaincode batch 3 adds key.register_session, key.revoke_session (session-only keys) and lab.supplement (supplementary laboratory reports). They are in the chaincode, and their backend and SDK support is being completed.
How the apps' operations map to actions
| App | Examples |
|---|---|
| Citizen app and portal | File a complaint (complaint.file), add information (complaint.add_info), sign within three days (complaint.sign by eSign or at the desk) |
| Police app | Register FIR (fir.register), Zero FIR (fir.register_zero), NCR (complaint.ncr), assign IO (fir.assign_io), diary (case.diary), seize evidence (evidence.seize with evidence.memo), dispatch to laboratory (lab.dispatch), final report (fir.final_report), reply to the court (police.status_reply) |
| Lab app | Accept or refuse a parcel (lab.accept, lab.refuse), advance a stage (lab.stage), save notes (lab.notes), seal (lab.seal), hand back (custody.offer) |
| Court app | Decide a probable duplicate (court.referral_decide), decide a closure report (court.closure_decide), take cognizance (court.cognizance), record an order (court.remand, court.bail, court.quash_record, court.transfer), ask for a status report (court.status_request) |
Sources: digifir-backend openapi/digifir.yaml (submitAction, SubmitAction, ActionReceipt, ActionPending), docs/architecture/BACKEND.md sections 3.1 to 3.4; digifir-chaincode docs/SPEC.md sections 1.7 and 10.