1
CannaManage 05 API
Patrick Plate edited this page 2026-04-06 11:21:47 +02:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

CannaManage REST API Specification v1.0

Base URL: https://{club-domain}/api/v1
Content-Type: application/json
Authentication: Bearer JWT token via Authorization: Bearer <token> header
Versioning: URL-based — current version /api/v1/, next version /api/v2/


Table of Contents

  1. Authentication & Conventions
  2. Error Format
  3. Pagination Envelope
  4. Custom Error Codes
  5. Auth Controller
  6. Club Controller
  7. Member Controller
  8. Distribution Controller
  9. Stock Controller
  10. Report Controller
  11. Compliance Controller

1. Authentication & Conventions

JWT Claims

The access token payload contains:

{
  "sub": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "tenant_id": "8e1a2b3c-4d5e-6f70-8091-a2b3c4d5e6f7",
  "role": "ADMIN",
  "email": "admin@gruener-daumen-ev.de",
  "iat": 1712345678,
  "exp": 1712349278
}

tenant_id is ALWAYS resolved from the JWT. It is never accepted as a request parameter or path variable. Any attempt to access data belonging to a different tenant returns 403 TENANT_VIOLATION.

Roles

Role Description
ADMIN Club administrator — full access to club data
MEMBER Regular club member — read-only access to own profile and distributions

Token Lifetimes

Token Lifetime
Access token 1 hour
Refresh token 30 days

2. Error Format

All error responses use application/problem+json format:

{
  "status": 400,
  "error": "BAD_REQUEST",
  "code": "QUOTA_EXCEEDED_MONTHLY",
  "message": "Monthly distribution quota of 50g exceeded for member.",
  "timestamp": "2026-04-06T10:15:30.000Z",
  "path": "/api/v1/distributions"
}
Field Type Description
status integer HTTP status code
error string HTTP status reason phrase
code string Machine-readable application error code (see §4)
message string Human-readable description
timestamp string ISO 8601 UTC timestamp
path string Request path that caused the error

3. Pagination Envelope

All list endpoints returning paginated results use this envelope:

{
  "content": [ "...array of items..." ],
  "page": 0,
  "size": 20,
  "totalElements": 150,
  "totalPages": 8
}

Standard query parameters for paginated endpoints:

Parameter Type Default Description
page integer 0 Zero-based page index
size integer 20 Page size (max: 100)
sort string varies Field name + direction, e.g. createdAt,desc

4. Custom Error Codes

Code HTTP Status Description
QUOTA_EXCEEDED_DAILY 422 Member has reached the 25 g/day distribution limit (KCanG §19 Abs. 5)
QUOTA_EXCEEDED_MONTHLY 422 Member has reached the monthly limit: 50 g (≥21 yrs) or 30 g (<21 yrs)
BATCH_RECALLED 422 The requested batch has an active contamination/recall flag
MEMBER_INACTIVE 422 Member status is SUSPENDED or EXPELLED — distributions blocked
MEMBER_UNDERAGE 422 Member date of birth indicates they are under 18 years old
DISTRIBUTION_IMMUTABLE 422 Attempt to modify or delete an existing distribution record (audit trail protected)
TENANT_VIOLATION 403 Requested resource belongs to a different tenant than the JWT claims
DSGVO_CONSENT_MISSING 422 Member has no DSGVO (GDPR) data processing consent on record
BATCH_INSUFFICIENT_STOCK 422 Batch does not have sufficient remaining quantity for the requested distribution
INVALID_CREDENTIALS 401 Email/password combination is incorrect
TOKEN_EXPIRED 401 Access or refresh token has expired
TOKEN_INVALID 401 Token signature is invalid or malformed
MEMBER_NOT_FOUND 404 No member with the given UUID exists in this tenant
BATCH_NOT_FOUND 404 No batch with the given UUID exists in this tenant
DISTRIBUTION_NOT_FOUND 404 No distribution with the given UUID exists in this tenant

5. Auth Controller (/auth)

5.1 POST /auth/login

Authenticate with email and password. Returns a short-lived access token and a long-lived refresh token.

Authentication: None required

Request Body:

{
  "email": "admin@gruener-daumen-ev.de",
  "password": "supersecret123"
}
Field Type Required Description
email string Club administrator email address
password string Plaintext password (transmitted over TLS)

Success Response — 200 OK:

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
  "tokenType": "Bearer",
  "expiresIn": 3600,
  "member": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "email": "admin@gruener-daumen-ev.de",
    "role": "ADMIN",
    "clubId": "8e1a2b3c-4d5e-6f70-8091-a2b3c4d5e6f7",
    "clubName": "Grüner Daumen e.V."
  }
}

Error Responses:

HTTP Status Error Code Condition
401 INVALID_CREDENTIALS Email not found or password does not match
400 BAD_REQUEST Missing required fields
429 TOO_MANY_REQUESTS Rate limit exceeded (5 failed attempts per 15 minutes)

5.2 POST /auth/refresh

Exchange a valid refresh token for a new access token.

Authentication: Refresh token in request body (not a Bearer token)

Request Body:

{
  "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..."
}

Success Response — 200 OK:

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "tokenType": "Bearer",
  "expiresIn": 3600
}

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_EXPIRED Refresh token has passed its 30-day lifetime
401 TOKEN_INVALID Refresh token signature is invalid or has been revoked
400 BAD_REQUEST refreshToken field is missing

5.3 POST /auth/logout

Invalidate the current refresh token. Access tokens remain valid until natural expiry (TTL-based, no server-side revocation).

Authentication: Bearer access token required

Request Body: None

Success Response — 204 No Content

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token is invalid or expired

6. Club Controller (/clubs)

6.1 GET /clubs/me

Retrieve the authenticated admin's club details. Tenant is resolved from JWT.

Authentication: Bearer token — role ADMIN

Success Response — 200 OK:

{
  "id": "8e1a2b3c-4d5e-6f70-8091-a2b3c4d5e6f7",
  "name": "Grüner Daumen e.V.",
  "registrationNumber": "VR 12345",
  "address": {
    "street": "Hanfstraße 42",
    "city": "Berlin",
    "postalCode": "10115",
    "state": "Berlin"
  },
  "contactEmail": "info@gruener-daumen-ev.de",
  "contactPhone": "+49 30 12345678",
  "foundedDate": "2024-04-01",
  "maxMembers": 500,
  "currentMemberCount": 87,
  "status": "ACTIVE",
  "createdAt": "2024-04-01T09:00:00.000Z",
  "updatedAt": "2026-03-15T14:22:00.000Z"
}

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

6.2 PUT /clubs/me

Update the authenticated admin's club details.

Authentication: Bearer token — role ADMIN

Request Body:

{
  "name": "Grüner Daumen e.V.",
  "registrationNumber": "VR 12345",
  "address": {
    "street": "Hanfstraße 42",
    "city": "Berlin",
    "postalCode": "10115",
    "state": "Berlin"
  },
  "contactEmail": "info@gruener-daumen-ev.de",
  "contactPhone": "+49 30 12345678",
  "maxMembers": 500
}
Field Type Required Description
name string Official club name as registered
registrationNumber string Vereinsregister number
address object Club registered address
contactEmail string Public contact email
contactPhone string Public contact phone
maxMembers integer Maximum membership capacity (default: 500, KCanG limit)

Success Response — 200 OK: Full club object (same as GET /clubs/me)

Error Responses:

HTTP Status Error Code Condition
400 BAD_REQUEST Validation failure (invalid email format, missing required fields)
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

6.3 GET /clubs/me/stats

Retrieve dashboard statistics for the authenticated club.

Authentication: Bearer token — role ADMIN

Success Response — 200 OK:

{
  "memberCount": {
    "total": 87,
    "active": 82,
    "pending": 3,
    "suspended": 1,
    "expelled": 1
  },
  "distributionsThisMonth": {
    "count": 214,
    "totalGrams": 3280.5,
    "uniqueMembers": 74
  },
  "stock": {
    "totalGrams": 12500.0,
    "activeBatches": 4,
    "strainCount": 6
  },
  "complianceAlerts": {
    "membersAtDailyLimit": 2,
    "membersAtMonthlyLimit": 5,
    "recalledBatchesActive": 0
  },
  "generatedAt": "2026-04-06T10:00:00.000Z"
}

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

7. Member Controller (/members)

7.1 GET /members

List all members of the authenticated club, paginated and optionally filtered by status.

Authentication: Bearer token — role ADMIN

Query Parameters:

Parameter Type Default Description
page integer 0 Page index (zero-based)
size integer 20 Page size
sort string lastName,asc Sort field and direction
status string (all) Filter by status: ACTIVE, PENDING, SUSPENDED, EXPELLED
search string (none) Full-text search against first name, last name, or member number

Success Response — 200 OK:

{
  "content": [
    {
      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "memberNumber": "GD-2024-001",
      "firstName": "Max",
      "lastName": "Mustermann",
      "email": "max@example.de",
      "status": "ACTIVE",
      "dateOfBirth": "1990-05-15",
      "joinDate": "2024-05-01",
      "monthlyQuotaGrams": 50,
      "createdAt": "2024-05-01T10:00:00.000Z"
    }
  ],
  "page": 0,
  "size": 20,
  "totalElements": 87,
  "totalPages": 5
}

Note: dateOfBirth is only included for ADMIN role. Member role responses omit it.

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

7.2 POST /members

Create a new club member. Generates a unique member number automatically.

Authentication: Bearer token — role ADMIN

Request Body:

{
  "firstName": "Max",
  "lastName": "Mustermann",
  "email": "max@example.de",
  "dateOfBirth": "1990-05-15",
  "address": {
    "street": "Musterstraße 1",
    "city": "Berlin",
    "postalCode": "10115",
    "state": "Berlin"
  },
  "phone": "+49 176 12345678",
  "dsgvoConsentDate": "2026-04-06",
  "joinDate": "2026-04-06",
  "notes": "Referred by member GD-2024-015"
}
Field Type Required Description
firstName string Legal first name
lastName string Legal last name
email string Contact email (must be unique within tenant)
dateOfBirth string ISO 8601 date — used for quota calculation (≥21: 50 g/month, <21: 30 g/month)
address object Member's registered home address
phone string Contact phone number
dsgvoConsentDate string Date member signed DSGVO consent form
joinDate string Official membership start date
notes string Internal admin notes (not visible to member)

Success Response — 201 Created:

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberNumber": "GD-2026-088",
  "firstName": "Max",
  "lastName": "Mustermann",
  "email": "max@example.de",
  "status": "ACTIVE",
  "dateOfBirth": "1990-05-15",
  "monthlyQuotaGrams": 50,
  "joinDate": "2026-04-06",
  "dsgvoConsentDate": "2026-04-06",
  "createdAt": "2026-04-06T10:30:00.000Z"
}

monthlyQuotaGrams is computed server-side based on age at time of creation: 50 g for members ≥21 years old, 30 g for members aged 1820.

Error Responses:

HTTP Status Error Code Condition
400 BAD_REQUEST Missing required fields or validation failure
409 CONFLICT Email already registered for another member in this tenant
422 MEMBER_UNDERAGE Computed age from dateOfBirth is under 18
422 DSGVO_CONSENT_MISSING dsgvoConsentDate is null or missing
422 QUOTA_EXCEEDED_DAILY Club has reached maximum member capacity (500 members per KCanG §15)
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

7.3 GET /members/{id}

Retrieve full details for a single member.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
id UUID Member's unique identifier

Success Response — 200 OK:

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberNumber": "GD-2026-088",
  "firstName": "Max",
  "lastName": "Mustermann",
  "email": "max@example.de",
  "phone": "+49 176 12345678",
  "dateOfBirth": "1990-05-15",
  "address": {
    "street": "Musterstraße 1",
    "city": "Berlin",
    "postalCode": "10115",
    "state": "Berlin"
  },
  "status": "ACTIVE",
  "monthlyQuotaGrams": 50,
  "joinDate": "2026-04-06",
  "dsgvoConsentDate": "2026-04-06",
  "notes": "Referred by member GD-2024-015",
  "createdAt": "2026-04-06T10:30:00.000Z",
  "updatedAt": "2026-04-06T10:30:00.000Z"
}

Error Responses:

HTTP Status Error Code Condition
404 MEMBER_NOT_FOUND No member with given UUID exists in this tenant
403 TENANT_VIOLATION Member UUID exists but belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

7.4 PUT /members/{id}

Update an existing member's details.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
id UUID Member's unique identifier

Request Body: Same structure as POST /members (all fields optional except those required for compliance)

Success Response — 200 OK: Full updated member object

Error Responses:

HTTP Status Error Code Condition
400 BAD_REQUEST Validation failure
404 MEMBER_NOT_FOUND No member with given UUID exists in this tenant
403 TENANT_VIOLATION Member UUID belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

7.5 DELETE /members/{id}

Soft-delete / expel a member. Sets status to EXPELLED and records an expulsion timestamp. No data is physically deleted — all historical records (distributions, etc.) are retained for compliance auditing.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
id UUID Member's unique identifier

Request Body:

{
  "reason": "Voluntary membership resignation",
  "effectiveDate": "2026-04-06"
}
Field Type Required Description
reason string Reason for expulsion/resignation (stored in audit log)
effectiveDate string ISO 8601 date when expulsion takes effect

Success Response — 200 OK:

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberNumber": "GD-2026-088",
  "status": "EXPELLED",
  "expelledAt": "2026-04-06T11:00:00.000Z",
  "expulsionReason": "Voluntary membership resignation"
}

Error Responses:

HTTP Status Error Code Condition
400 BAD_REQUEST Missing reason or effectiveDate
404 MEMBER_NOT_FOUND No member with given UUID in this tenant
409 CONFLICT Member already has EXPELLED status
403 TENANT_VIOLATION Member UUID belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

7.6 GET /members/me

Retrieve the authenticated member's own profile.

Authentication: Bearer token — role MEMBER

Success Response — 200 OK:

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberNumber": "GD-2026-088",
  "firstName": "Max",
  "lastName": "Mustermann",
  "email": "max@example.de",
  "status": "ACTIVE",
  "monthlyQuotaGrams": 50,
  "joinDate": "2026-04-06"
}

dateOfBirth, address, notes, and internal fields are omitted from member self-view.

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid
422 MEMBER_INACTIVE Member account is suspended or expelled

7.7 GET /members/{id}/quota

Get the current month's quota usage for a member.

Authentication: Bearer token — role ADMIN, or MEMBER accessing their own ID

Path Parameters:

Parameter Type Description
id UUID Member's unique identifier

Query Parameters:

Parameter Type Default Description
month string current month ISO 8601 year-month, e.g. 2026-04

Success Response — 200 OK:

{
  "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberNumber": "GD-2026-088",
  "month": "2026-04",
  "monthlyLimitGrams": 50,
  "distributedThisMonthGrams": 22.5,
  "remainingMonthlyGrams": 27.5,
  "dailyLimitGrams": 25,
  "distributedTodayGrams": 5.0,
  "remainingTodayGrams": 20.0,
  "distributionCount": 3,
  "quotaExceeded": false,
  "nearLimit": false
}

nearLimit is true when remaining grams ≤ 10 g (monthly) or ≤ 5 g (daily).

Error Responses:

HTTP Status Error Code Condition
403 FORBIDDEN MEMBER role accessing another member's quota
404 MEMBER_NOT_FOUND No member with given UUID in this tenant
403 TENANT_VIOLATION Member UUID belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid

7.8 GET /members/{id}/distributions

Get distribution history for a member, paginated.

Authentication: Bearer token — role ADMIN, or MEMBER accessing their own ID

Path Parameters:

Parameter Type Description
id UUID Member's unique identifier

Query Parameters:

Parameter Type Default Description
page integer 0 Page index
size integer 20 Page size
sort string distributedAt,desc Sort field and direction
from string ISO 8601 date filter start, e.g. 2026-01-01
to string ISO 8601 date filter end

Success Response — 200 OK:

{
  "content": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "distributedAt": "2026-04-06T09:30:00.000Z",
      "strainName": "Blue Dream",
      "batchId": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
      "quantityGrams": 5.0,
      "notes": null
    }
  ],
  "page": 0,
  "size": 20,
  "totalElements": 42,
  "totalPages": 3
}

Error Responses:

HTTP Status Error Code Condition
403 FORBIDDEN MEMBER role accessing another member's distributions
404 MEMBER_NOT_FOUND No member with given UUID in this tenant
403 TENANT_VIOLATION Member UUID belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid

8. Distribution Controller (/distributions)

8.1 GET /distributions

List all distributions for the authenticated club, filterable by date range and member.

Authentication: Bearer token — role ADMIN

Query Parameters:

Parameter Type Default Description
page integer 0 Page index
size integer 20 Page size
sort string distributedAt,desc Sort field and direction
from string ISO 8601 date filter start
to string ISO 8601 date filter end
memberId UUID Filter by specific member
batchId UUID Filter by specific batch

Success Response — 200 OK:

{
  "content": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "distributedAt": "2026-04-06T09:30:00.000Z",
      "member": {
        "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "memberNumber": "GD-2026-088",
        "firstName": "Max",
        "lastName": "Mustermann"
      },
      "strain": {
        "id": "c4d5e6f7-a8b9-0123-cdef-456789abcdef",
        "name": "Blue Dream",
        "thcPercent": 18.5,
        "cbdPercent": 0.3
      },
      "batchId": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
      "batchCode": "BATCH-2026-003",
      "quantityGrams": 5.0,
      "handedOutBy": "admin@gruener-daumen-ev.de",
      "notes": null,
      "correctionNotes": []
    }
  ],
  "page": 0,
  "size": 20,
  "totalElements": 214,
  "totalPages": 11
}

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

8.2 POST /distributions

Record a new distribution. All compliance rules are checked before the record is created. This operation is atomic — either all checks pass and the record is written, or the entire request fails.

Authentication: Bearer token — role ADMIN

Request Body:

{
  "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "batchId": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
  "quantityGrams": 5.0,
  "notes": "Member requested half portion"
}
Field Type Required Description
memberId UUID Receiving member's UUID
batchId UUID Stock batch UUID to draw from
quantityGrams number Amount in grams (max 2 decimal places)
notes string Optional admin note for this distribution

Success Response — 201 Created:

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "distributedAt": "2026-04-06T09:30:00.000Z",
  "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberNumber": "GD-2026-088",
  "batchId": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
  "batchCode": "BATCH-2026-003",
  "strainName": "Blue Dream",
  "quantityGrams": 5.0,
  "remainingMonthlyQuotaGrams": 22.5,
  "remainingDailyQuotaGrams": 15.0,
  "handedOutBy": "admin@gruener-daumen-ev.de"
}

Compliance Checks (in order of precedence):

  1. Member exists and belongs to this tenant
  2. Member status is ACTIVE
  3. Member has DSGVO consent on record
  4. Batch exists, belongs to this tenant, and is not recalled
  5. Batch has sufficient remaining stock
  6. quantityGrams ≤ remaining daily quota (25 g/day - already distributed today)
  7. quantityGrams ≤ remaining monthly quota (50 g or 30 g - already distributed this month)

Error Responses:

HTTP Status Error Code Condition
422 MEMBER_INACTIVE Member status is SUSPENDED or EXPELLED
422 DSGVO_CONSENT_MISSING Member has no DSGVO consent date
422 BATCH_RECALLED Batch has an active contamination recall
422 BATCH_INSUFFICIENT_STOCK Batch has less remaining stock than quantityGrams
422 QUOTA_EXCEEDED_DAILY Distribution would exceed 25 g/day limit
422 QUOTA_EXCEEDED_MONTHLY Distribution would exceed monthly quota
404 MEMBER_NOT_FOUND Member UUID not found in this tenant
404 BATCH_NOT_FOUND Batch UUID not found in this tenant
403 TENANT_VIOLATION Member or batch belongs to different tenant
400 BAD_REQUEST quantityGrams ≤ 0 or > 25
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

KCanG Compliance Note: Single distributions of more than 25 g are rejected even if monthly quota remains. The legal daily limit is 25 g per person per calendar day (KCanG §19 Abs. 5).


8.3 GET /distributions/{id}

Retrieve a single distribution record by ID.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
id UUID Distribution's unique identifier

Success Response — 200 OK: Full distribution object (same structure as list items in §8.1)

Error Responses:

HTTP Status Error Code Condition
404 DISTRIBUTION_NOT_FOUND No distribution with given UUID in this tenant
403 TENANT_VIOLATION Distribution belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

8.4 POST /distributions/{id}/notes

Add a correction note to an existing distribution. Distribution records themselves are immutable — amount, member, batch, and timestamp cannot be changed. Correction notes provide an auditable annotation layer for errors or clarifications.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
id UUID Distribution's unique identifier

Request Body:

{
  "note": "Entry error — scale was miscalibrated. Actual weight was approximately 4.8g. Scale recalibrated and verified on 2026-04-06.",
  "correctedBy": "admin@gruener-daumen-ev.de"
}
Field Type Required Description
note string Correction note text (max 2000 characters)
correctedBy string Email of admin entering the correction

Success Response — 201 Created:

{
  "noteId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
  "distributionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "note": "Entry error — scale was miscalibrated. Actual weight was approximately 4.8g.",
  "correctedBy": "admin@gruener-daumen-ev.de",
  "createdAt": "2026-04-06T11:15:00.000Z"
}

Error Responses:

HTTP Status Error Code Condition
404 DISTRIBUTION_NOT_FOUND No distribution with given UUID in this tenant
403 TENANT_VIOLATION Distribution belongs to a different tenant
400 BAD_REQUEST Missing note field or note exceeds 2000 chars
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

Audit Trail Note: Correction notes do NOT change the original distribution amount. For compliance reporting, the original amount is always used. Correction notes appear in distribution detail views and audit exports but do not affect quota calculations.


9. Stock Controller (/stock)

9.1 GET /stock/strains

List all cannabis strains registered in the club's catalogue.

Authentication: Bearer token — role ADMIN

Query Parameters:

Parameter Type Default Description
page integer 0 Page index
size integer 50 Page size
active boolean Filter by active strains only

Success Response — 200 OK:

{
  "content": [
    {
      "id": "c4d5e6f7-a8b9-0123-cdef-456789abcdef",
      "name": "Blue Dream",
      "variety": "HYBRID",
      "thcPercent": 18.5,
      "cbdPercent": 0.3,
      "description": "Classic hybrid, earthy and sweet aroma",
      "active": true,
      "createdAt": "2025-01-15T10:00:00.000Z"
    }
  ],
  "page": 0,
  "size": 50,
  "totalElements": 6,
  "totalPages": 1
}

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

9.2 POST /stock/strains

Register a new cannabis strain in the club's catalogue.

Authentication: Bearer token — role ADMIN

Request Body:

{
  "name": "OG Kush",
  "variety": "INDICA",
  "thcPercent": 22.0,
  "cbdPercent": 0.1,
  "description": "Classic indica, piney and citrus notes"
}
Field Type Required Description
name string Strain name
variety string SATIVA, INDICA, or HYBRID
thcPercent number THC content percentage (0100)
cbdPercent number CBD content percentage (0100)
description string Optional descriptive notes

Success Response — 201 Created: Full strain object

Error Responses:

HTTP Status Error Code Condition
409 CONFLICT Strain with same name already exists in this tenant
400 BAD_REQUEST Invalid variety value or missing required fields
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

9.3 GET /stock/batches

List all stock batches for the authenticated club.

Authentication: Bearer token — role ADMIN

Query Parameters:

Parameter Type Default Description
page integer 0 Page index
size integer 20 Page size
status string Filter: AVAILABLE, DEPLETED, RECALLED
strainId UUID Filter by strain

Success Response — 200 OK:

{
  "content": [
    {
      "id": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
      "batchCode": "BATCH-2026-003",
      "strain": {
        "id": "c4d5e6f7-a8b9-0123-cdef-456789abcdef",
        "name": "Blue Dream"
      },
      "initialQuantityGrams": 2000.0,
      "remainingQuantityGrams": 850.5,
      "status": "AVAILABLE",
      "harvestDate": "2026-02-15",
      "labTestDate": "2026-03-01",
      "labTestReference": "LAB-2026-1234",
      "thcPercent": 19.2,
      "addedAt": "2026-03-10T08:00:00.000Z"
    }
  ],
  "page": 0,
  "size": 20,
  "totalElements": 4,
  "totalPages": 1
}

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

9.4 POST /stock/batches

Add a new stock batch for a registered strain.

Authentication: Bearer token — role ADMIN

Request Body:

{
  "strainId": "c4d5e6f7-a8b9-0123-cdef-456789abcdef",
  "initialQuantityGrams": 2000.0,
  "harvestDate": "2026-02-15",
  "labTestDate": "2026-03-01",
  "labTestReference": "LAB-2026-1234",
  "thcPercent": 19.2,
  "cbdPercent": 0.4,
  "notes": "Batch from indoor cultivation cycle 3"
}
Field Type Required Description
strainId UUID Strain this batch belongs to
initialQuantityGrams number Starting weight in grams
harvestDate string ISO 8601 date of harvest
labTestDate string ISO 8601 date of laboratory analysis
labTestReference string Lab report reference number (audit requirement)
thcPercent number Actual THC % from lab test
cbdPercent number Actual CBD % from lab test
notes string Internal notes

Success Response — 201 Created: Full batch object (same as list item above)

Error Responses:

HTTP Status Error Code Condition
400 BAD_REQUEST Missing required fields or invalid quantities
404 strainId not found in this tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

9.5 GET /stock/batches/{id}

Retrieve full details for a specific batch including distribution history summary.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
id UUID Batch unique identifier

Success Response — 200 OK:

{
  "id": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
  "batchCode": "BATCH-2026-003",
  "strain": {
    "id": "c4d5e6f7-a8b9-0123-cdef-456789abcdef",
    "name": "Blue Dream",
    "variety": "HYBRID"
  },
  "initialQuantityGrams": 2000.0,
  "remainingQuantityGrams": 850.5,
  "distributedQuantityGrams": 1149.5,
  "distributionCount": 48,
  "status": "AVAILABLE",
  "harvestDate": "2026-02-15",
  "labTestDate": "2026-03-01",
  "labTestReference": "LAB-2026-1234",
  "thcPercent": 19.2,
  "cbdPercent": 0.4,
  "notes": "Batch from indoor cultivation cycle 3",
  "recallInfo": null,
  "addedAt": "2026-03-10T08:00:00.000Z",
  "updatedAt": "2026-04-06T09:30:00.000Z"
}

recallInfo is null for non-recalled batches. See §9.6 for recall structure.

Error Responses:

HTTP Status Error Code Condition
404 BATCH_NOT_FOUND No batch with given UUID in this tenant
403 TENANT_VIOLATION Batch belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

9.6 POST /stock/batches/{id}/recall

Flag a batch as recalled due to contamination or safety concerns. This immediately prevents any new distributions from this batch. Members who received product from this batch can be identified via GET /reports/recall/{batchId}.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
id UUID Batch unique identifier

Request Body:

{
  "reason": "Pesticide residue detected above legal threshold in repeat lab test",
  "detectedAt": "2026-04-06",
  "severity": "HIGH",
  "labReference": "LAB-2026-9876"
}
Field Type Required Description
reason string Description of the contamination/recall reason
detectedAt string ISO 8601 date contamination was detected
severity string LOW, MEDIUM, or HIGH
labReference string Lab report reference for contamination finding

Success Response — 200 OK:

{
  "id": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
  "batchCode": "BATCH-2026-003",
  "status": "RECALLED",
  "recallInfo": {
    "reason": "Pesticide residue detected above legal threshold",
    "detectedAt": "2026-04-06",
    "severity": "HIGH",
    "labReference": "LAB-2026-9876",
    "recalledAt": "2026-04-06T11:30:00.000Z",
    "recalledBy": "admin@gruener-daumen-ev.de",
    "affectedMemberCount": 23
  }
}

Error Responses:

HTTP Status Error Code Condition
404 BATCH_NOT_FOUND No batch with given UUID in this tenant
409 CONFLICT Batch is already in RECALLED status
403 TENANT_VIOLATION Batch belongs to a different tenant
400 BAD_REQUEST Missing required fields or invalid severity value
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

9.7 GET /stock/summary (ADMIN view)

Retrieve a complete stock summary showing totals by strain.

Authentication: Bearer token — role ADMIN

Success Response — 200 OK:

{
  "totalAvailableGrams": 12500.0,
  "activeBatches": 4,
  "strains": [
    {
      "strainId": "c4d5e6f7-a8b9-0123-cdef-456789abcdef",
      "strainName": "Blue Dream",
      "availableGrams": 850.5,
      "batchCount": 1
    },
    {
      "strainId": "d5e6f7a8-b9c0-1234-defa-5678901bcdef",
      "strainName": "OG Kush",
      "availableGrams": 11649.5,
      "batchCount": 3
    }
  ],
  "recalledBatches": 0,
  "generatedAt": "2026-04-06T10:00:00.000Z"
}

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

9.8 GET /stock/summary (MEMBER view)

Retrieve stock availability for members — shows strain availability status only. No quantities are exposed to members.

Authentication: Bearer token — role MEMBER

Success Response — 200 OK:

{
  "strains": [
    {
      "strainName": "Blue Dream",
      "variety": "HYBRID",
      "available": true
    },
    {
      "strainName": "OG Kush",
      "variety": "INDICA",
      "available": true
    }
  ],
  "generatedAt": "2026-04-06T10:00:00.000Z"
}

MEMBER view is served by the same endpoint path — the response schema differs based on the JWT role claim. No grams, no batch codes, no THC percentages beyond what's in the strain catalogue.

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid

10. Report Controller (/reports)

All report endpoints are ADMIN-only. Reports are tenant-scoped — all data is filtered to the requesting club's tenant_id.

10.1 GET /reports/monthly

Generate the monthly compliance report. Supports JSON (default), PDF, and CSV output.

Authentication: Bearer token — role ADMIN

Query Parameters:

Parameter Type Default Description
month string current month ISO 8601 year-month, e.g. 2026-03
format string json Output format: json, pdf, csv

Success Response — 200 OK (format=json):

{
  "reportId": "RPT-2026-04-GD",
  "clubName": "Grüner Daumen e.V.",
  "month": "2026-03",
  "generatedAt": "2026-04-06T10:00:00.000Z",
  "summary": {
    "totalDistributions": 214,
    "totalGramsDistributed": 3280.5,
    "uniqueMembers": 74,
    "activeMembers": 82,
    "newMembers": 3,
    "expelledMembers": 1
  },
  "complianceSummary": {
    "quotaViolationsDetected": 0,
    "recallsTriggered": 0,
    "dsgvoNonCompliantMembers": 0
  },
  "distributions": [
    {
      "date": "2026-03-01",
      "count": 9,
      "totalGrams": 132.5
    }
  ]
}

Success Response — format=pdf:

  • Content-Type: application/pdf
  • Content-Disposition: attachment; filename="cannamanage-report-2026-03.pdf"
  • Binary PDF body

Success Response — format=csv:

  • Content-Type: text/csv; charset=UTF-8
  • Content-Disposition: attachment; filename="cannamanage-report-2026-03.csv"
  • UTF-8 CSV with BOM for Excel compatibility

Error Responses:

HTTP Status Error Code Condition
400 BAD_REQUEST Invalid month format or format value
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

10.2 GET /reports/members

Export the full member list — intended for presentation to authorities during official inspections.

Authentication: Bearer token — role ADMIN

Query Parameters:

Parameter Type Default Description
format string json Output format: json, pdf, csv
status string ACTIVE Filter by status
asOf string today ISO 8601 date — membership as of this date

Success Response — 200 OK (format=json):

{
  "clubName": "Grüner Daumen e.V.",
  "asOf": "2026-04-06",
  "totalCount": 82,
  "members": [
    {
      "memberNumber": "GD-2024-001",
      "firstName": "Max",
      "lastName": "Mustermann",
      "dateOfBirth": "1990-05-15",
      "joinDate": "2024-05-01",
      "status": "ACTIVE",
      "dsgvoConsentDate": "2024-05-01"
    }
  ]
}

This report includes date of birth and DSGVO consent date as required by KCanG inspection protocols.

Error Responses:

HTTP Status Error Code Condition
400 BAD_REQUEST Invalid format or status value
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

10.3 GET /reports/recall/{batchId}

Generate a recall impact report for a specific batch — identifies all members who received product from the recalled batch.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
batchId UUID Recalled batch unique identifier

Query Parameters:

Parameter Type Default Description
format string json Output format: json, pdf, csv

Success Response — 200 OK (format=json):

{
  "batchCode": "BATCH-2026-003",
  "strainName": "Blue Dream",
  "recallReason": "Pesticide residue detected above legal threshold",
  "recalledAt": "2026-04-06T11:30:00.000Z",
  "severity": "HIGH",
  "affectedMembers": [
    {
      "memberNumber": "GD-2024-001",
      "firstName": "Max",
      "lastName": "Mustermann",
      "email": "max@example.de",
      "phone": "+49 176 12345678",
      "totalReceivedGrams": 15.0,
      "lastDistributionDate": "2026-04-05",
      "distributionCount": 3
    }
  ],
  "totalAffectedMembers": 23,
  "totalAffectedGrams": 345.5,
  "generatedAt": "2026-04-06T11:35:00.000Z"
}

Error Responses:

HTTP Status Error Code Condition
404 BATCH_NOT_FOUND No batch with given UUID in this tenant
400 BAD_REQUEST Invalid format value
403 TENANT_VIOLATION Batch belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

11. Compliance Controller (/compliance)

11.1 GET /compliance/quota/{memberId}

Check the current compliance status for a specific member's quota. Intended for real-time verification before handing out product at the counter.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
memberId UUID Member's unique identifier

Success Response — 200 OK:

{
  "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberNumber": "GD-2026-088",
  "memberStatus": "ACTIVE",
  "dsgvoConsentPresent": true,
  "quota": {
    "month": "2026-04",
    "monthlyLimitGrams": 50,
    "distributedThisMonthGrams": 22.5,
    "remainingMonthlyGrams": 27.5,
    "dailyLimitGrams": 25,
    "distributedTodayGrams": 0.0,
    "remainingTodayGrams": 25.0
  },
  "canReceive": true,
  "blockingReasons": []
}

Blocked member example:

{
  "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberNumber": "GD-2026-089",
  "memberStatus": "ACTIVE",
  "dsgvoConsentPresent": true,
  "quota": {
    "month": "2026-04",
    "monthlyLimitGrams": 30,
    "distributedThisMonthGrams": 30.0,
    "remainingMonthlyGrams": 0.0,
    "dailyLimitGrams": 25,
    "distributedTodayGrams": 0.0,
    "remainingTodayGrams": 25.0
  },
  "canReceive": false,
  "blockingReasons": ["QUOTA_EXCEEDED_MONTHLY"]
}

blockingReasons is an array — multiple blocks can apply simultaneously (e.g., both MEMBER_INACTIVE and DSGVO_CONSENT_MISSING).

Error Responses:

HTTP Status Error Code Condition
404 MEMBER_NOT_FOUND No member with given UUID in this tenant
403 TENANT_VIOLATION Member belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

11.2 GET /compliance/check

Dry-run compliance pre-check for a proposed distribution. Nothing is recorded — this is a read-only validation endpoint used to preview whether a specific distribution would pass all rules.

Authentication: Bearer token — role ADMIN

Query Parameters:

Parameter Type Required Description
memberId UUID Member who would receive
batchId UUID Batch to distribute from
quantityGrams number Proposed quantity in grams

Example request:

GET /api/v1/compliance/check?memberId=3fa85f64-...&batchId=f1e2d3c4-...&quantityGrams=10

Success Response — 200 OK (all checks pass):

{
  "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "batchId": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
  "quantityGrams": 10.0,
  "allowed": true,
  "checks": {
    "memberActive": true,
    "dsgvoConsentPresent": true,
    "batchAvailable": true,
    "batchNotRecalled": true,
    "batchSufficientStock": true,
    "dailyQuotaOk": true,
    "monthlyQuotaOk": true
  },
  "quotaAfter": {
    "remainingMonthlyGrams": 17.5,
    "remainingTodayGrams": 15.0
  }
}

Success Response — 200 OK (checks fail — still HTTP 200, not an error):

{
  "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "batchId": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
  "quantityGrams": 30.0,
  "allowed": false,
  "checks": {
    "memberActive": true,
    "dsgvoConsentPresent": true,
    "batchAvailable": true,
    "batchNotRecalled": true,
    "batchSufficientStock": true,
    "dailyQuotaOk": false,
    "monthlyQuotaOk": true
  },
  "violations": ["QUOTA_EXCEEDED_DAILY"],
  "quotaAfter": null
}

This endpoint always returns 200 OK. The allowed field indicates pass/fail. Use violations array to display which rules would be broken.

Error Responses (true errors only):

HTTP Status Error Code Condition
400 BAD_REQUEST Missing required query parameters or quantityGrams ≤ 0
404 MEMBER_NOT_FOUND Member UUID not found in this tenant
404 BATCH_NOT_FOUND Batch UUID not found in this tenant
403 TENANT_VIOLATION Member or batch belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

Appendix A: Member Status Lifecycle

PENDING → ACTIVE → SUSPENDED → ACTIVE   (reinstatement possible)
ACTIVE  → EXPELLED                       (permanent, no reinstatement via API)
PENDING → EXPELLED                       (rejected application)
Status Distributions allowed Login allowed
PENDING No No
ACTIVE Yes Yes
SUSPENDED No Yes (read-only)
EXPELLED No No

Appendix B: KCanG Compliance Reference

Key limits implemented in the distribution compliance engine (as of KCanG 2024):

Rule Limit Applied To
Daily distribution 25 g/day All adult members
Monthly distribution (≥21 years) 50 g/month Members aged 21+
Monthly distribution (1820 years) 30 g/month Members aged 1820
Minimum age 18 years All members
Maximum club members 500 Per Anbauvereinigung
Simultaneous memberships 1 club Per person (enforced externally)

Age is calculated at the time of each distribution request, not at membership creation time. A member who turns 21 during the month automatically becomes eligible for the 50 g limit on their birthday.


Appendix C: Tenant Isolation Guarantee

Every database query includes an implicit WHERE tenant_id = ? clause derived from the JWT. The following guarantees hold:

  1. A request authenticated with tenant A's JWT cannot read, modify, or delete data belonging to tenant B — even if a valid UUID belonging to tenant B is provided.
  2. Any attempt to access cross-tenant resources returns 403 TENANT_VIOLATION.
  3. Cross-tenant violations are logged server-side to the security audit log.
  4. Tenant ID is never accepted from the request (body, headers, or query parameters) — it is always derived from the validated JWT signature.