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
- POST
/api/payments/staff-crypto-payroll/enrollmentsv3.117Enroll 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).
- DELETE
/api/payments/staff-crypto-payroll/enrollmentsv3.118Unenroll 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.
- POST
/api/payments/staff-crypto-payroll/disbursementsv3.111Enqueue 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.
- GET
/api/payments/staff-crypto-payroll/disbursements/:idv3.112Single-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 } - GET
/api/payments/staff-crypto-payroll/disbursementsv3.116Period-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" } - GET
/api/payments/staff-crypto-payroll/healthv3.136Liveness 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.
| Code | Meaning | Recovery |
|---|---|---|
| 200 | Idempotent replay / successful read / successful unenroll. | — |
| 201 | First enrollment / first disbursement enqueue. | — |
| 400 | Per-field validation (missing/wrong types) or invalid_json. | Fix the request body and resend. |
| 401 | Missing 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. |
| 404 | enrollment_not_found / enrollment_not_active. The (dispensary, staff) tuple is not enrolled. | POST to /enrollments first. If the staff was unenrolled, re-POST. |
| 422 | wallet_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. |
| 500 | db_error or unexpected. Includes a `requestId` to correlate with CannAgent server logs. | Retry with backoff. Persistent → contact support with requestId. |
| 503 | payment_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
- Federal — IRS Notice 2014-21: Crypto wages are W-2 income at fair-market-value at payment time. Same withholding + reporting as USD. Staff acknowledges this at enrollment time (irsAcknowledged: true is hard-required).
- State (WA):Payroll is payroll regardless of asset. WA L&I, PFML, SUI all calculate off the USD-equivalent gross wages (grossWagesCents on the disbursement row), NOT the USDC amount.
- Federal money transmission (BSA/AML): The USD → USDC conversion routes through CannAgent Payment Services LLC, which is pursuing a WA DFI Money Transmitter License (~Q1 2027 target). Pre-MTL the master feature flag is off and these endpoints return 503.
- Wallet screening: TRM-Labs-style address risk screening at enrollment time AND at send-time (status can change between enrollment and payday — sanctions listing added, mixer association observed). Sanctioned tier returns 422; review tier needs compliance officer clear.
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
requestIdto correlate with CannAgent server logs. Passx-request-idif 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.