For operators · technical reference

Staff crypto payroll API

Six HTTP endpoints. Bearer auth (except the no-auth liveness probe). Idempotent on (dispensary, staff) for enrollments and (dispensary, staff, payroll-period) for disbursements. The operator’s HRIS / payroll stack calls these; CannAgent’s every-5-minute cron drives the on-chain leg.

Authentication

Every request requires a Bearer token in the Authorization header. The token is the value of the PAYROLL_API_KEY environment variable on the CannAgent deploy. Comparison is timing-safe.

Authorization: Bearer <your-key-here>

v1 is single-tenant: one shared key per CannAgent deploy. Phase 6 adds per-dispensary keys with a rotate-key surface in operator settings (target alongside WA DFI MTL approval Q1 2027).

Endpoints

  1. POST/api/payments/staff-crypto-payroll/enrollmentsv3.117

    Enroll a staff member into crypto payroll. Idempotent on (dispensaryId, staffUserId): re-POST with a new wallet address rotates the wallet (re-screens at enrollment time). irsAcknowledged must be literal true — staff acknowledges IRS Notice 2014-21 (W-2 income at FMV).

    Request

    curl -X POST https://app.cannagent.ai/api/payments/staff-crypto-payroll/enrollments \
      -H "Authorization: Bearer $PAYROLL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "dispensaryId": "550e8400-e29b-41d4-a716-446655440000",
        "staffUserId":  "550e8400-e29b-41d4-a716-446655440001",
        "staffWalletAddress": "0x1234567890abcdef1234567890abcdef12345678",
        "cryptoPct": 100,
        "irsAcknowledged": true,
        "rail": "evm-base"
      }'

    Response

    {
      "id": "eb74…",
      "enabled": true,
      "cryptoPct": 100,
      "asset": "USDC",
      "rail": "evm-base",
      "walletScreeningResult": "clear",
      "irs2014_21AcknowledgedAt": "2026-05-08T12:00:00Z",
      "enrolledAt": "2026-05-08T12:00:00Z",
      "isExisting": false,
      "requestId": "enrollment-1715169600000-ab12cd"
    }

    201 on first enrollment, 200 on idempotent replay / wallet rotation. 422 on wallet_blocked (sanctioned) or wallet_review_required (review-tier flag).

  2. DELETE/api/payments/staff-crypto-payroll/enrollmentsv3.118

    Unenroll a staff member. Soft-delete: enabled=false + unenrolledAt=NOW. Row stays in the table for audit + so future re-enrollment can be distinguished from first-time. Reason field is persisted to the audit log — do NOT pass HR-investigation notes (PII surface).

    Request

    curl -X DELETE \
      "https://app.cannagent.ai/api/payments/staff-crypto-payroll/enrollments?dispensaryId=550e8400-e29b-41d4-a716-446655440000&staffUserId=550e8400-e29b-41d4-a716-446655440001&reason=voluntary+opt-out" \
      -H "Authorization: Bearer $PAYROLL_API_KEY"

    Response

    {
      "id": "eb74…",
      "enabled": false,
      "unenrolledAt": "2026-05-08T12:00:00Z",
      "requestId": "unenroll-1715169600000-ab12cd"
    }

    200 on success, 404 enrollment_not_found if no row matches the (dispensary, staff) tuple.

  3. POST/api/payments/staff-crypto-payroll/disbursementsv3.111

    Enqueue a per-paycheck disbursement. Operator's payroll system POSTs once per staff per pay period. Idempotent on (dispensaryId, staffUserId, payrollPeriodId): retries return 200 + the existing row instead of double-paying. The cron then picks the row up and drives it through the rail.

    Request

    curl -X POST https://app.cannagent.ai/api/payments/staff-crypto-payroll/disbursements \
      -H "Authorization: Bearer $PAYROLL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "dispensaryId":      "550e8400-e29b-41d4-a716-446655440000",
        "staffUserId":       "550e8400-e29b-41d4-a716-446655440001",
        "payrollPeriodId":   "2026-W19",
        "payrollPeriodStart":"2026-05-04T00:00:00Z",
        "payrollPeriodEnd":  "2026-05-10T23:59:59Z",
        "grossWagesCents":   180000
      }'

    Response

    {
      "id": "a1b2…",
      "status": "pending",
      "usdcAmountCents":  180000,
      "cryptoPortionPct": 100,
      "grossWagesCents":  180000,
      "payrollPeriodId":  "2026-W19",
      "enqueuedAt": "2026-05-08T12:00:00Z",
      "isExisting": false,
      "requestId": "payroll-1715169600000-ab12cd"
    }

    201 on first enqueue, 200 on idempotent replay. 404 enrollment_not_active if the staff member is not enrolled.

  4. GET/api/payments/staff-crypto-payroll/disbursements/:idv3.112

    Single-row reconciliation. Operator polls this after enqueueing to track lifecycle (pending → processing → settled / failed) and trigger ACH fallback on failed. Returns the FULL wallet address + tx hash for on-chain reconciliation.

    Request

    curl https://app.cannagent.ai/api/payments/staff-crypto-payroll/disbursements/a1b2… \
      -H "Authorization: Bearer $PAYROLL_API_KEY"

    Response

    {
      "id": "a1b2…",
      "status": "settled",
      "payrollPeriodId": "2026-W19",
      "grossWagesCents": 180000,
      "usdcAmountCents": 180000,
      "cryptoPortionPct": 100,
      "asset": "USDC",
      "rail": "evm-base",
      "staffWalletAddress": "0x1234…5678",
      "sendTimeScreeningResult": "clear",
      "txHash": "0xabc…",
      "txConfirmations": 2,
      "enqueuedAt":          "2026-05-08T12:00:00Z",
      "processingStartedAt": "2026-05-08T12:00:30Z",
      "settledAt":           "2026-05-08T12:01:00Z",
      "failureReason": null
    }
  5. GET/api/payments/staff-crypto-payroll/disbursementsv3.116

    Period-end reconciliation. Roll up all disbursements for a payroll period in one query. Hard cap 500 rows; `capped: true` flag signals the need to paginate (pagination ships when a real operator hits the cap).

    Request

    curl "https://app.cannagent.ai/api/payments/staff-crypto-payroll/disbursements?payrollPeriodId=2026-W19&dispensaryId=550e8400-e29b-41d4-a716-446655440000&status=settled" \
      -H "Authorization: Bearer $PAYROLL_API_KEY"

    Response

    {
      "disbursements": [ /* full disbursement rows */ ],
      "total": 24,
      "capped": false,
      "statusCounts": {
        "pending":    0,
        "processing": 0,
        "settled":    24,
        "failed":     0,
        "reversed":   0
      },
      "payrollPeriodId": "2026-W19"
    }
  6. GET/api/payments/staff-crypto-payroll/healthv3.136

    Liveness probe. NO auth required — operator's HRIS / monitoring stack uses this for uptime checks without holding a PAYROLL_API_KEY. Returns the master + sandbox flag state so the operator can distinguish 'CannAgent up but payment-systems flag-disabled' from 'CannAgent down entirely'. Poll no faster than every 60s.

    Request

    curl https://app.cannagent.ai/api/payments/staff-crypto-payroll/health

    Response

    {
      "ok": true,
      "paymentSystemsEnabled": false,
      "sandboxMode": true,
      "dbConfigured": true,
      "ranAtIso": "2026-05-08T12:00:00.000Z",
      "ratelimitGuidance": "Poll no faster than every 60 seconds..."
    }

    Always 200 unless the platform itself is down. Reveals only flag-state — no PII, no dispensary data.

Status codes

The same code matrix is returned by every endpoint. Recovery steps assume the operator’s payroll stack is doing the calling.

CodeMeaningRecovery
200Idempotent replay / successful read / successful unenroll.
201First enrollment / first disbursement enqueue.
400Per-field validation (missing/wrong types) or invalid_json.Fix the request body and resend.
401Missing or wrong Bearer token. PAYROLL_API_KEY unset on the deploy is also 401 (fail-closed).Confirm the header and the deploy-side env var.
404enrollment_not_found / enrollment_not_active. The (dispensary, staff) tuple is not enrolled.POST to /enrollments first. If the staff was unenrolled, re-POST.
422wallet_blocked (sanctioned-tier wallet screen) / wallet_review_required (review-tier; manual compliance officer clear required).Use a different wallet address, or contact CannAgent compliance for review-tier clearance.
500db_error or unexpected. Includes a `requestId` to correlate with CannAgent server logs.Retry with backoff. Persistent → contact support with requestId.
503payment_systems_disabled (master flag off, e.g., pre-MTL) or db_unavailable (DATABASE_URL unset).Fall back to ACH-only payroll for the crypto leg. Master flag flips when WA DFI MTL approves (target Q1 2027).

Compliance frame

Operational notes

Idempotency:
Enrollments dedupe on (dispensaryId, staffUserId); disbursements dedupe on (dispensaryId, staffUserId, payrollPeriodId). Safe to retry on transient errors.
Request IDs:
Every response includes a requestId to correlate with CannAgent server logs. Pass x-request-id if you want the operator-side trace id to flow through.
Lifecycle:
pending → processing → settled / failed. pending → reversed (admin clawback before send). settled → reversed (admin clawback after send; rare). failed and reversed are terminal.
Cron cadence:
The disbursement sweep runs every 5 minutes (hard-cap 50 rows/run). At a 1,000-staff scale, a payroll run of all staff settles within ~25 minutes of the operator’s final POST.