Record Consent
Append-only log of user consent decisions (TOS, privacy, cookies, etc). Captures IP and user-agent for compliance audit.
POST /api/v1/users/consent — 🔑 Bearer
Records a single immutable consent decision in the ConsentRecord log. Captures the user's choice (accepted: true | false), the document type and version, plus req.ip and the User-Agent header for compliance audit. Append-only — there is no update/delete; revising consent for the same document is a new row.
Compliance-grade. The IP and user-agent are stored to prove who, when, and from where a consent was given. The frontend sends only the document type / version / accepted flag; the request metadata (req.ip, req.headers['user-agent']) is captured server-side.
Request
Body — RecordConsentDto
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
documentType | string | ✓ | IsString() | Free-form key — typical values: tos, privacy, cookies, marketing-emails |
documentVersion | string | ✓ | IsString() | Semver-ish version of the document being agreed to (e.g. 2.1, 2026-04-29) |
accepted | boolean | ✓ | IsBoolean() | true for an opt-in, false for an explicit decline (declines are also logged for audit) |
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
201 Created — ApiResponseOf<ConsentRecordedDto>
{
"success": true,
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}| Field | Type | Notes |
|---|---|---|
id | string (UUID) | ConsentRecord.id — useful only for tracing in support tickets |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | (DTO validation) | Missing / wrong-typed fields |
401 | (guard) | Missing / invalid bearer token |
Side effects
- Generate
id = randomUUID(). - Insert
ConsentRecord { id, userId, documentType, documentVersion, accepted, ipAddress: req.ip, userAgent: req.headers['user-agent'] }. No deduplication — calling twice with the same document/version creates two rows. - Audit log:
[consent] {documentType} v{documentVersion} accepted/declined by user {userId}. - No further side effects — does not flip any flag on
User. Document gating is read separately via the consent history (orhasConsent()server-side helper).
Code samples
curl -X POST https://api.bio.re/api/v1/users/consent \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"documentType": "tos",
"documentVersion": "2.1",
"accepted": true
}'type RecordConsentInput = {
documentType: string;
documentVersion: string;
accepted: boolean;
};
async function recordConsent(accessToken: string, input: RecordConsentInput): Promise<string> {
const res = await fetch('https://api.bio.re/api/v1/users/consent', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Record consent failed'), {
code: json?.error?.code,
});
}
return json.data.id as string;
}import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useRecordConsent() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (input: RecordConsentInput) => {
const res = await fetch('/api/v1/users/consent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Record consent failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data.id as string;
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['users', 'consent'] });
},
});
}Try it
Authorization
bearer In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
curl -X POST "https://loading/api/v1/users/consent" \ -H "Content-Type: application/json" \ -d '{ "documentType": "string", "documentVersion": "string", "accepted": true }'{
"success": true,
"data": {
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
}
}{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/user/user.controller.ts | 248–261 (recordConsent) |
| DTO (request) | apps/api-core/src/modules/user/dto/index.ts | 20–24 (RecordConsentDto) |
| DTO (response) | apps/api-core/src/modules/user/dto/user-client-response.dto.ts | 182–185 (ConsentRecordedDto) |
| Service | apps/api-core/src/modules/user/user.service.ts | 719–734 (recordConsent) |
| Prisma model | packages/prisma/prisma/schema.prisma | ConsentRecord |
Request Deletion (Legacy)
Older /users/delete endpoint — same effect as POST /gdpr/delete but additionally requires the current account password and returns only the scheduled date.
Get Consent History
List the user's last 100 consent decisions in reverse-chronological order. Useful for "your consent history" UIs and compliance exports.