Compliance - Views Documentation
Location: api/nextango/apps/compliance/views.py, api/nextango/apps/compliance/views_kyc.py, api/nextango/apps/compliance/cannabis/views.py
Last Updated: 2026-07-06
Overview
The compliance app exposes endpoints for purchase-limit status, age verification, regulated customer onboarding (POS + storefront), pending-credential review, KYC vendor webhooks, cannabis compliance reports, and the Metrc verification gate on purchase orders. Authentication is required on every endpoint except the storefront self-registration submission and the KYC vendor webhook, both of which have their own authorisation paths.
The purchase-limit and age-verification view classes live in the cannabis container (compliance/cannabis/views.py); their routes stay registered on the agnostic compliance/urls.py, so the public paths do not change. Report and Metrc endpoints route through compliance/cannabis/urls.py. Primary keys are UUIDs across these models, path and body identifiers are UUID strings, not integers.
GET /compliance/purchase-limits/status/
Returns the current purchase limit consumption for a customer at a store. Passes an empty prospective cart, so the response reflects what has already been consumed in the current period, not a hypothetical future check.
Auth: Required
Class: PurchaseLimitStatusView
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_id | string (UUID) | Yes | Customer PK |
store_id | string (UUID) | Yes | Store PK |
transaction_type | str | No | 'adult_use' (default) or 'medical' |
Response 200 OK (adult-use):
{
"allowed": true,
"period_type": "adult_use_daily",
"categories": {
"flower": {"consumed": "0.0000", "limit": "2.5", "remaining": "2.5", "allowed": true},
"concentrate": {"consumed": "0.0000", "limit": "15.0", "remaining": "15.0", "allowed": true},
"edible": {"consumed": "0.0000", "limit": "15000.0", "remaining": "15000.0", "allowed": true}
}
}
Response 200 OK (medical):
{
"allowed": true,
"period_type": "medical_monthly",
"categories": {
"medical_thc_mg": {"consumed": "0.0000", "limit": "90000.0", "remaining": "90000.0", "allowed": true}
}
}
Error 400:
{"error": "customer_id and store_id are required"}
POST /compliance/age-verification/
Records an age verification attempt at the POS and updates customer verification state. On approval with a date of birth, enqueues a Celery task to upsert a government_id_verification credential.
Auth: Required
Class: AgeVerificationView
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
customer_id | string (UUID) | Yes | Customer PK |
store_id | string (UUID) | Yes | Store PK |
employee_id | string (UUID) | Yes | Employee PK (the verifying employee) |
verification_type | str | Yes | government_id, passport, military_id, or medical_card |
result | str | Yes | approved, rejected, rejected_expired, or rejected_invalid |
id_expiry_date | str | No | YYYY-MM-DD, document expiry |
date_of_birth | str | No | YYYY-MM-DD, captured on first presentation |
notes | str | No | Free-text notes |
Response 201 Created:
{
"event_id": "3f2b1e5a-9c74-4a1e-bf03-2d6c8e1a7b40",
"result": "approved",
"is_verified_for_session": true,
"customer_type": "adult_use"
}
For medical customers, two additional fields are included:
{
"event_id": "3f2b1e5a-9c74-4a1e-bf03-2d6c8e1a7b40",
"result": "approved",
"is_verified_for_session": true,
"customer_type": "medical",
"medical_card_valid": true,
"medical_card_status": "active"
}
Error 400:
{"error": "Missing required fields: ['result']"}
GET /compliance/age-verification/status/
Checks whether a customer is currently verified for the session without recording a new event.
Auth: Required
Class: AgeVerificationStatusView
Query parameters:
| Parameter | Type | Required |
|---|---|---|
customer_id | string (UUID) | Yes |
store_id | string (UUID) | Yes |
Response 200 OK:
{
"customer_id": "8a1c4d2e-5b39-4e77-9f21-6c0b3a9d1e58",
"store_id": "1b7e9f04-2a63-4c8d-b5e1-7d90a2c46f83",
"is_verified_for_session": true,
"customer_type": "adult_use"
}
For medical customers, medical_card_valid is included:
{
"customer_id": "8a1c4d2e-5b39-4e77-9f21-6c0b3a9d1e58",
"store_id": "1b7e9f04-2a63-4c8d-b5e1-7d90a2c46f83",
"is_verified_for_session": true,
"customer_type": "medical",
"medical_card_valid": true
}
Error 400:
{"error": "customer_id and store_id are required"}
POST /compliance/onboarding/regulated-customer/
Creates a user and their credentials atomically through RegulatedCustomerOnboardingService with channel='pos'. Credentials in the store's secondary_review_credential_types list enter pending_employee_review; all others become active.
Auth: IsAuthenticated + IsEmployeeOrManager
Class: RegulatedCustomerOnboardingView
Request body:
{
"user_data": {
"email": "...",
"name": "...",
"first_name": "...",
"last_name": "...",
"store_id": "1b7e9f04-2a63-4c8d-b5e1-7d90a2c46f83"
},
"credentials": [
{"credentialType": "government_id_verification", "payload": {...}}
],
"channel": "pos"
}
Response 201 Created:
{
"pk": "a4d9c2f1-6e83-4b57-9a02-1c7e5d8b3f60",
"email": "[email protected]",
"name": "Jane Doe",
"credentials": [{"_key": "...", "credentialType": "...", "status": "active", "payload": {...}}]
}
Error 400: Invalid channel, missing store, missing required credential type, unknown credential type, invalid payload.
POST /compliance/onboarding/storefront-customer/
Unauthenticated storefront self-registration endpoint. Delegates to RegulatedCustomerOnboardingService with channel='storefront'. Every submitted credential is written with status='pending_verification' and awaits either employee review or KYC vendor confirmation.
Auth: AllowAny
Class: StorefrontCustomerOnboardingView
Request body:
{
"user_data": {"email": "...", "name": "...", "store_id": "1b7e9f04-2a63-4c8d-b5e1-7d90a2c46f83"},
"credentials": [
{"credentialType": "government_id_verification", "payload": {...}}
]
}
Response 201 Created: Same shape as the POS onboarding endpoint.
Error 400: Invalid input (missing store, unknown credential type, invalid payload). Error 409: Email already in use.
POST /compliance/onboarding/credentials/{credential_key}/approve/
Approves a pending credential. The acting user must pass EmployeeComplianceService.check_can_process_sale (active DCC agent permit + cleared background check). Only stored verificationData keys are verifiedDocumentId, verifiedBiometricId, reviewNotes, anything else in the request is dropped.
Auth: IsAuthenticated + IsEmployeeOrManager
Class: ApprovePendingCredentialView
URL parameter: credential_key, the credential's _key.
Request body:
{
"verificationData": {
"verifiedDocumentId": "...",
"verifiedBiometricId": "...",
"reviewNotes": "..."
}
}
Response 200 OK: Updated credential dict with status='active', approvedBy, approvedAt, and the filtered verificationData.
Error 400: Employee not authorised, credential not found, credential not in a pending status.
POST /compliance/onboarding/credentials/{credential_key}/reject/
Rejects a pending credential. Same employee and state preconditions as approve.
Auth: IsAuthenticated + IsEmployeeOrManager
Class: RejectPendingCredentialView
URL parameter: credential_key, the credential's _key.
Request body:
{"reason": "Document expired and not re-submitted"}
Response 200 OK: Updated credential dict with status='rejected', rejectedBy, rejectedAt, rejectionReason.
Error 400: Employee not authorised, credential not found, credential not in a pending status.
GET /compliance/onboarding/pending-credentials/
Lists every credential in a pending status (pending_verification, pending_employee_review, pending_vendor_response) whose submittedAtStoreId matches the requested store. The requesting user must be an employee or manager with a store assignment covering that store.
Auth: IsAuthenticated + IsEmployeeOrManager
Class: PendingCredentialsListView
Query parameters:
| Parameter | Type | Required |
|---|---|---|
store_id | string (UUID) | Yes |
Response 200 OK:
[
{
"user_id": "a4d9c2f1-6e83-4b57-9a02-1c7e5d8b3f60",
"user_name": "Jane Doe",
"credential_key": "ab12cd34...",
"credential_type": "government_id_verification",
"status": "pending_verification",
"submittedAtStoreId": "1b7e9f04-2a63-4c8d-b5e1-7d90a2c46f83"
}
]
Error 400: store_id missing.
Error 403: User has no employee or manager role with a store assignment covering this store.
POST /compliance/kyc/persona/webhook/
Persona inquiry-status webhook. Verifies the Persona-Signature HMAC header over the raw request body, then applies the adapter's VerificationResult to the owning credential. Full signing flow is documented in Webhooks; full adapter behaviour is documented in Adapters.
Auth: AllowAny (HMAC signature is the authorisation)
Class: PersonaWebhookView
Response 200 OK: {"status": "active" | "rejected"} on resolved outcomes, or {"status": <current>, "detail": "unresolved"} for in-flight events that do not yet change status.
Error 400: Invalid JSON body, missing reference-id.
Error 401: Missing/invalid signature, stale timestamp (>300s skew), or webhook secret not configured.
Error 404: Credential key from the payload does not match any stored credential.
GET /compliance/cannabis/reports/
Paginated list of compliance reports the caller may read. A report is visible to its requester and to the compliance manager of its target store.
Auth: IsAuthenticated
Class: ComplianceReportListCreateView
Pagination: 50 per page (ComplianceReportPagination).
Response 200 OK: Standard DRF paginated payload wrapping ComplianceReport records (id, report_type, store, date_from, date_to, status, requested_by, generated_at, error_message).
POST /compliance/cannabis/reports/
Creates a new ComplianceReport (status pending) and enqueues the generate_compliance_report Celery task. Store-scoped requests require the caller to be the compliance manager of that store; chain-wide requests (store=null) resolve to the caller's manager-accessible stores inside the generator.
Auth: IsAuthenticated
Class: ComplianceReportListCreateView
Request body:
{
"report_type": "daily_sales",
"store": "1b7e9f04-2a63-4c8d-b5e1-7d90a2c46f83",
"date_from": "2026-04-01",
"date_to": "2026-04-20"
}
report_type is one of: daily_sales, purchase_limit_audit, age_verification_audit, recall_destruction_audit, cash_reconciliation.
Response 201 Created: Created ComplianceReport record.
GET /compliance/cannabis/reports/{id}/
Polls a report's status. Subject to the same read-scoping as the list endpoint.
Auth: IsAuthenticated
Class: ComplianceReportDetailView
Response 200 OK: ComplianceReport record.
Error 404: Report does not exist or caller is not permitted to read it.
GET /compliance/cannabis/reports/{id}/download/
Redirects to the generated output_file.url. Returns 404 unless the report is in ready status and has a file attached.
Auth: IsAuthenticated
Class: ComplianceReportDownloadView
Response 302 Found: Redirect to the file URL.
Response 404: Report not ready (status != ready) or caller not permitted to read it.
POST /compliance/cannabis/purchase-orders/{pk}/metrc-verify/
Transitions a purchase order from AWAITING_COMPLIANCE_VERIFICATION to RECEIVED after a compliance officer has visually verified the Metrc package tags. Writes verifiedAt and verifiedBy (user PK) to the compliance_verification JSONField.
Auth: Required
Class: PurchaseOrderMetrcVerifyView
URL parameter: pk, PurchaseOrder PK
Request body: Empty ({})
Response 200 OK:
{
"po_number": "PO-2025-0042",
"status": "received",
"verified_at": "2025-03-05T14:32:00.000000+00:00"
}
Error 400 (wrong status):
{"error": "PO is not awaiting compliance verification (current status: received)"}