Skip to main content

Troubleshooting: proof-of-deletion certificate errors on GET /compliance/dsar/erasure-requests/{id}/certificate

The certificate endpoint issues the signed proof-of-deletion certificate for a completed Article-17 erasure: the receipt a data subject or supervisory authority accepts as evidence that the right to erasure was fulfilled. This page covers every response the endpoint can return, how to read a 200 that looks wrong, how to verify the certificate you hold, and what to do while the erasure is still in flight. The normative reference is Data Subject Access Requests; the step-by-step operational sequence lives in the Proof-of-Deletion Certificate Runbook.

What the certificate is, and who asks for it

Article 17 obliges you to erase personal data when the right applies; Article 12 obliges you to demonstrate to the data subject that you did. The certificate is that demonstration in portable form: a dated, signed, per-subsystem statement of what was purged, when, under which request, and against which tamper-evident audit anchor. A data subject asks for it to close their request; a supervisory authority or auditor asks for it as GDPR Art. 17 evidence in an investigation or proceeding. Orbit signs the artifact’s integrity. Whether the certificate discharges your legal obligation in a given proceeding is a determination for your counsel, consistent with Orbit’s tenant-owned compliance posture.

The endpoint, end to end

GET /compliance/dsar/erasure-requests/{id}/certificate requires an owner or admin credential. On a 200 it derives the certificate on demand from the immutable executed request row and its audit-chain anchor, self-signs it with a detached HMAC-SHA256 tag, and returns { "certificate": ..., "signature": ... }. Nothing is persisted: re-issuing at any time reproduces the same certificate_id for the same execution. The failing states, in the order the endpoint checks them:
  • 401 UNAUTHORIZED — no valid credential on the request. Attach your API key and retry.
  • 403 INSUFFICIENT_PERMISSIONS — the credential is valid but its role is below owner/admin; the message reads This action requires one of: owner, admin. Switch to an owner- or admin-scoped credential. This is a permissions posture, not a defect.
  • 404 NOT_FOUND — no erasure request with this id exists in your tenant. Certificates are tenant-scoped: a request that belongs to another organization is indistinguishable from one that never existed. Re-copy the id from GET /compliance/dsar/erasure-requests.
  • 409 ERASURE_NOT_EXECUTED — the request exists but is not executed. The message names the live status (pending, executing, cancelled, or failed). Only a completed erasure can be certified; work the grid below.
The lifecycle is pending → executing → executed, with terminal branches cancelled and failed. The executor ticks every five minutes; once the cooling-off window lapses, a healthy erasure moves from pending to executed in minutes, not hours.

Error grid

A 200 with an unexpected status-shaped field

The certificate carries no lifecycle status field. A 200 means the backing row was executed at read time, and the fields that read as surprises are honest flags, not failures:
  • records_erased.by_resource is null for a subsystem whose table does not exist on your tenant: nothing existed there to purge. A number, including 0, is a confirmed count.
  • records_erased.total is null when every purge ran but no count could be confirmed. It does not mean nothing was deleted.
  • destination_propagation.total: 0 is a signed, honest “no external destinations were notified” (internal-only erasure), not a missing section.
  • destination_propagation.failed or skipped entries mean a downstream destination rejected or was not configured for the fan-out. Re-run propagation, then issue again: the re-issue keeps the same certificate_id and folds the updated trail under a fresh issued_at and signature.
  • attempt_count above 1 means an earlier attempt hit a transient fault and the executor retried. The committed result is still all-or-nothing.
If you genuinely expected a different request, match on erasure_request_id and data_subject_contact_id before trusting the file you opened: certificates are derived per request, and a stale id in your script is the usual cause of a “wrong” 200.

Verifying the certificate against the executed erasure

Two independent checks, in increasing order of strength: Audit-chain cross-reference (yours to run). The certificate’s audit_log_id names a row in your organization’s audit chain with action gdpr.erasure_executed. Its resource_counts must equal records_erased.by_resource, and its timestamp must equal executed_at. Every issuance also appends a compliance.dsar_erasure_certificate_issued row naming the certificate_id, so your audit export shows who issued which certificate and when. A certificate whose anchor is missing or disagrees should be treated as altered or mis-cited. The certificate_id itself is deterministic, derived from the request id and executed_at, so re-issuing the same executed request always yields the same id: the digest you filed stays comparable against any later re-issue. Signature recompute (held by the signing-key holder). The tag is a self-signature by Devotel Orbit over the canonicalised body; the recipe is published for transparency:
  1. Canonicalise the certificate object with RFC 8785 (JCS) style rules: object keys sorted ascending, no insignificant whitespace.
  2. Compute HMAC-SHA256 over the string orbit.dsar.erasure-certificate.v1.<canonical-json>.
  3. Base64url-encode and compare with signature.value using a timing-safe equality check.
The domain-separation context (orbit.dsar.erasure-certificate.v1) is mixed into every tag, and this is the deterministic-build contract the certificate service documents: the signed byte sequence depends only on the immutable executed row, the audit anchor, and issued_at, never on read timing or row and key order. A mismatched or malformed tag verifies false; verification never fails open.

Reading subject-bound data while the request is pending

The certificate exists only after completion. While an erasure is pending or executing, the subject-bound data still exists, and the path to read it is the Article-15 access export, not the certificate:
  • GET /compliance/dsar/{id}/decrypted returns the plaintext access bundle for a completed, unexpired access request, decrypted for the verified controller (owner or admin) and audit-logged.
  • It applies to access-class requests only. An Article-17 erasure produces no access export and returns 409 DSAR_NOT_ACCESS_REQUEST: the two request types close different obligations and their artifacts do not substitute for each other.
  • If the pending request is a delete and you need to inspect what will be destroyed, read a completed access export for the same subject where one exists; otherwise wait for the erasure to finalize and present the certificate.

Your control, your posture

Issuing the certificate is a tenant-triggered control: Orbit makes the artifact available on every executed erasure, and you decide when to issue it, whom to hand it to, and how long it lives in your privacy register. The platform does not auto-issue, auto-publish, or auto-expire certificates on your behalf, and no regulator interaction is driven by Orbit. Whether a presented certificate discharges your obligation is your counsel’s determination, not a platform default.

Worked example

Issue the certificate and keep the artifact verbatim:
A 200 returns the certificate + signature pair (the runbook shows the full body). If instead you receive:
the request is mid-execution: poll until it reads executed, then re-run the same call.

See also