Ask an organisation for everything it holds on you¶
A Data Subject Access Request — DSAR — is the legal right to demand a copy of every piece of personal data an organisation holds about you, and to be told what it is doing with it. This page generates that letter: the correct statute section for the jurisdiction, the deadline the law gives the recipient, and an Ed25519 signature the receiving data protection officer can check offline.
The right exists in every jurisdiction arche ships a statute pack for. Nigeria's NDPA-2023 grants it at section 34, South Africa's POPIA at section 23, Kenya's DPA at section 26(a), Ghana's DPA at section 35. On paper a citizen simply asks and the organisation has thirty days — twenty-one in Ghana — to answer.
In practice almost nobody asks, and the reason is bureaucratic rather than legal. Exercising the right means identifying which organisations hold your data, finding each one's data protection officer, drafting a request that cites the correct section of the correct statute, sending it through a channel that counts, tracking the response, and escalating to the regulator when it never arrives. Six steps, each of which quietly assumes you are a lawyer.
arche.workflow.DSARWorkflow does steps three and four. It drafts the letter with the right citation and the right deadline, and it signs the draft so the recipient can verify the request came from the holder of a particular key and has not been altered since. You review it and you send it. It never sends anything itself — see dispatch, and why it is draft-only at the bottom of the page.
The workflow¶
from arche.sign import generate_keypair
from arche.workflow import DSAROrganization, DSARRequestor, DSARWorkflow
# Citizen-held key: generate once, store locally. arche never holds it.
citizen_key = generate_keypair()
workflow = DSARWorkflow(
jurisdiction="NG", # resolves NDPA-2023
requestor=DSARRequestor(
name="Adesola Okonkwo",
identifier_label="NIN",
identifier_value="12345678901",
email="adesola@example.com",
phone="+234 803 555 7890",
),
request_type="access", # see request types below
targets=[
DSAROrganization(name="Sterling Bank Limited",
dpo_email="dpo@sterlingbank.ng"),
DSAROrganization(name="MTN Nigeria Communications",
dpo_email="dpo@mtn.ng"),
],
)
result = workflow.run(citizen_key)
for draft in result.drafts:
print(draft.citation) # the statute section cited
print(draft.deadline) # when the clock runs out
print(draft.letter_text) # ready to email
print(draft.signed_envelope) # JWS, for the DPO to verify
One draft comes back per target. DSARResult carries drafts, jurisdiction, statute, request_type, dispatch_mode, sent_at and tracking; each DSARDraft carries target, citation, statute_short, deadline, letter_text and signed_envelope.
Request types¶
Five request types, matching the vocabulary the NDPA, POPIA, Kenya DPA and Ghana DPA share with the GDPR.
| Request type | What it asks for |
|---|---|
access |
A copy of all personal data held about the requestor |
rectification |
Correction of inaccurate or incomplete data |
erasure |
Deletion of personal data ("right to be forgotten") |
portability |
Personal data in a structured, machine-readable format |
objection |
Cessation of processing for specified purposes |
The citation is chosen for you¶
Two axes decide which section appears in the letter: the jurisdiction and the request type. Both come from the statute YAML in arche/policy/statutes/, not from a table in this page.
from arche.sign import generate_keypair
from arche.workflow import DSAROrganization, DSARRequestor, DSARWorkflow
citizen_key = generate_keypair()
def draft(jurisdiction, request_type="access"):
result = DSARWorkflow(
jurisdiction=jurisdiction,
requestor=DSARRequestor(name="Adesola Okonkwo", identifier_label="NIN",
identifier_value="12345678901",
email="adesola@example.com"),
request_type=request_type,
targets=[DSAROrganization(name="Acme Ltd", dpo_email="dpo@acme.test")],
).run(citizen_key)
return result, result.drafts[0]
for jurisdiction in ("NG", "ZA", "KE", "GH"):
result, d = draft(jurisdiction)
print(f"{jurisdiction} {result.statute:12} {d.citation}")
print()
for request_type in ("access", "rectification", "erasure", "portability",
"objection"):
_, d = draft("NG", request_type)
print(f" {request_type:15} {d.citation}")
NG NDPA-2023 NDPA-2023 s.34 (Right of Access)
ZA POPIA POPIA s.23 (Access to personal information)
KE KENYA-DPA Kenya DPA s.26(a) (Right of Access)
GH GHANA-DPA Ghana DPA s.35 (Access to personal data)
access NDPA-2023 s.34 (Right of Access)
rectification NDPA-2023 s.35 (Right to Rectification)
erasure NDPA-2023 s.36 (Right to Erasure)
portability NDPA-2023 s.38 (Right to Data Portability)
objection NDPA-2023 s.37 (Right to Object)
The deadline is the statute's, unless you shorten it¶
| Jurisdiction | Statutory deadline |
|---|---|
| NDPA-2023 (Nigeria) | 30 days |
| POPIA (South Africa) | 30 days |
| Kenya DPA | 30 days |
| Ghana DPA | 21 days |
draft.deadline is a timezone-aware datetime computed from the statute pack and stamped into the signed envelope's expires_at. Pass deadline_days= to shorten it — there is no reason to lengthen it, since the statute already sets the maximum.
What a draft looks like¶
Real output from result.drafts[0].letter_text, run on the date in the first line. Only the date and the version string change between runs.
2026-08-09
Data Protection Officer
Sterling Bank Limited
[Address on file]
Subject: Data Subject Access Request under Nigeria Data Protection Act 2023 (NDPA-2023)
Dear Data Protection Officer,
I, Adesola Okonkwo, identified by NIN (12345678901),
hereby exercise my right under NDPA-2023 s.34 (Right of Access) to access to all personal data that you hold concerning me.
Identity verification details:
Full name: Adesola Okonkwo
NIN: 12345678901
Email: adesola@example.com
Phone: +234 803 555 7890
You are required to respond to this request within 30 days of
receipt, as provided by Nigeria Data Protection Act 2023 (NDPA-2023). Please confirm receipt within
7 working days and provide an estimated response date.
If you fail to respond within the statutory window, or if I am not
satisfied with your response, I reserve the right to lodge a complaint
with the Nigeria Data Protection Commission (NDPC).
Yours faithfully,
Adesola Okonkwo
adesola@example.com
---
This letter was generated by arche-core v0.3.0a1 (NDPA / POPIA /
Kenya DPA / Ghana DPA compliant DSAR drafting workflow). The accompanying
signed envelope (arche+envelope/v1, Ed25519 over canonical JSON) provides
cryptographic provenance and tamper evidence.
The ragged line breaks and the misaligned NIN: row are the template's, not a transcription slip. The letter is a draft you edit before sending, and tidying the template is outstanding work rather than a feature.
What the signature proves, and what it does not¶
The envelope beside each draft is a JWS over canonical JSON. A data protection officer who receives one can check it without contacting anybody.
from arche.sign import VerifyExtractWorkflow
# The DPO has the letter and the envelope, and nothing else.
untrusted = VerifyExtractWorkflow(
require_purpose=f"dsar_{result.request_type}",
require_jurisdiction=result.jurisdiction,
).process(result.drafts[0].signed_envelope)
# The DPO has previously been given the citizen's public key, out of band.
pinned = VerifyExtractWorkflow(
public_key=citizen_key.public_key,
require_purpose=f"dsar_{result.request_type}",
require_jurisdiction=result.jurisdiction,
).process(result.drafts[0].signed_envelope)
for label, v in (("no key", untrusted), ("pinned key", pinned)):
print(f"{label:11} valid={v.signature_valid!s:5} "
f"trusted={v.signature_trusted!s:5} key_source={v.key_source}")
print("issuer :", pinned.issuer_did)
print("deadline :", pinned.envelope.expires_at)
no key valid=True trusted=False key_source=self-asserted
pinned key valid=True trusted=True key_source=pinned
Read those two rows carefully, because the difference is the whole security model. valid says the signature matches the key that was resolved. trusted says that key came from somewhere the verifier controls. With no key supplied, the verifier falls back to the did:key the token names about itself — which any impostor can also do, with their own keypair and a matching kid. That check proves the letter has not been altered since it was signed. It proves nothing at all about who signed it. Attest has the forged-token demonstration.
Altering the letter breaks the envelope outright:
from arche.sign import VerifyExtractWorkflow
tampered = result.drafts[0].signed_envelope[:-4] + "AAAA"
VerifyExtractWorkflow().process(tampered)
So what a DPO actually learns from a verified envelope is: the request was signed by the holder of this specific key, at this timestamp, for this purpose and this jurisdiction, with this deadline, and neither the letter nor its metadata has changed since. Whether that key belongs to Adesola Okonkwo is an identity question the signature does not answer, and the letter's identity-verification block is where the organisation's own checks begin.
Dispatch, and why it is draft-only¶
dispatch_mode accepts exactly one value, "draft_only", and it is the default. The workflow writes the letter; you read it and you send it.
That is a deliberate constraint rather than an unfinished feature. A workflow that emailed statutory demands on a citizen's behalf would be one miscited section or one wrong DPO address away from doing real damage in that citizen's name, and the person carrying the consequence is not the person who wrote the code. Autonomous dispatch would need consent mechanics, a delivery audit trail, and a bounce-and-escalation path that none of this ships today.
The organisation side is also absent: arche drafts requests, it does not help a controller answer them. That is a different product with different obligations, and it is on the roadmap rather than in this release.
What's next¶
- Sign, share, extract — the envelope primitives this workflow composes
- Attest: the signature on the decision — why
trustedis the field to check - API overview — the public workflow surface