Developer API
Public, flat-fee data APIs over verified state cannabis-registry data. Built for KYB and compliance teams at banks and credit unions serving cannabis businesses, compliance/RegTech vendors, insurers, and fintech platforms that need license verification inside onboarding and monitoring flows — plus operators embedding a live license badge and analysts piping new-license feeds into Slack or a reader.
Plain HTTPS in; JSON, SVG, and RSS out. No SDK required. This page documents the endpoints exactly as deployed — parameter rules, quotas, and error strings below are taken from the running code, and the base URLs shown are this site’s own.
- Single license verification — free JSON lookup, no key
- API keys — create a key, check usage
- Batch verification — the KYB workflow, up to 200 licenses per call
- Embeddable license badge — live SVG status on your own site
- New-licenses RSS feed — newest active licenses per state
- Query & feed API · Fair use & attribution · Data sources & freshness
Single license verification
Check one license number against the registry mirror. Public and anonymous — meant for humans, spot checks, and low-volume integrations. For programmatic volume, use batch verification with an API key.
GET https://project.canalinx.com/wp-json/platform/v1/verify
| Parameter | In | Required | Notes |
|---|---|---|---|
license | query | yes | License number as issued. Matching is punctuation-insensitive (dashes, spaces, and periods are ignored); at least 3 alphanumeric characters after normalization. |
state | query | yes | Two-letter USPS state code, e.g. CA. Non-letters are stripped; the value must resolve to exactly two letters. |
Example request
curl "https://project.canalinx.com/wp-json/platform/v1/verify?license=C10-0000123-LIC&state=CA"Response — record found
{
"found": true,
"license_number": "C10-0000123-LIC",
"status": "active",
"type": "retailer",
"state": "CA",
"city": "Sacramento",
"county": "Sacramento",
"expiry_date": "2027-01-31",
"last_synced": "2026-07-06 03:14:22",
"freshness_score": 98,
"disclaimer": "Registry mirror for informational use; confirm current standing with the issuing authority for legal purposes."
}Response — no match
{
"found": false,
"state": "CA",
"note": "No match in covered registry data. Absence here is not proof a license does not exist — check state coverage."
}Errors
HTTP 400
{"code":"bad_input","message":"license and two-letter state required.","data":{"status":400}}
HTTP 429 (per-IP throttle; response carries a Retry-After header, window 3600 s)
{"code":"rate_limited","message":"Too many attempts. Please wait and try again."}Auth & quota
- Auth: none — fully public.
- Quota: a per-IP throttle of 30 requests per hour (3600-second window from the first request) covers the verification routes; exceeding it returns HTTP 429 with a
Retry-Afterheader. Batch calls pass through the same per-IP throttle, so sustained volume belongs on the keyed batch endpoint.
API keys
Keys unlock the batch endpoint and metered daily quotas. Any signed-in account can create one free key; the raw key is shown once (we store only a SHA-256 hash). Free keys carry a 100 requests/day quota; higher quotas come as flat-priced tiers at launch.
Create a key
POST https://project.canalinx.com/wp-json/platform/v1/apikey-create
| Parameter | In | Required | Notes |
|---|---|---|---|
label | body (form or JSON) | no | Human label for the key, up to 80 characters. Defaults to "default". |
Requires a signed-in account: a normal browser session, or HTTP Basic auth with a WordPress application password as shown below. Unauthenticated calls return HTTP 401 (auth_required, “Sign in to create an API key.”). A second active key at the free tier returns HTTP 400 (limit, “One active key per account at the free tier.”).
curl -X POST "https://project.canalinx.com/wp-json/platform/v1/apikey-create" \
-u "owner@yourcompany.com:APPLICATION-PASSWORD" \
-d "label=kyb-pilot"{
"ok": true,
"api_key": "pfk_9f2c4a1e0b7d68d31c5a2e4f6b8d0a1c2e3f4a5b",
"note": "Store it now — shown ONCE, we keep only a hash. 100 requests/day free."
}pfk_ + 40 hex characters (44 characters total) and are returned exactly once. Lost keys cannot be recovered — only replaced.Check usage
GET https://project.canalinx.com/wp-json/platform/v1/apikey-usage
Signed-in; lists your keys with a hint (first 8 characters), label, daily quota, active flag, and today’s call count.
curl "https://project.canalinx.com/wp-json/platform/v1/apikey-usage" \
-u "owner@yourcompany.com:APPLICATION-PASSWORD"{
"ok": true,
"keys": [
{
"key_hint": "pfk_9f2c",
"label": "kyb-pilot",
"daily_quota": "100",
"is_active": "1",
"today": "37"
}
]
}Batch verification
The KYB workflow: submit up to 200 license + state pairs in one call — one CSV’s worth — and get statuses back in order. Requires an API key.
POST https://project.canalinx.com/wp-json/platform/v1/verify-batch
| Parameter | In | Required | Notes |
|---|---|---|---|
items | body (JSON) | yes | Array of 1-200 objects: {"license": "...", "state": "XX"}. Same normalization rules as single verify apply per item. Each item consumes one quota unit. |
X-Api-Key | header | one of the two | Your API key. Recommended: headers keep keys out of URLs and access logs. |
key | query | one of the two | API key as a query parameter; alternative to the X-Api-Key header. |
Example request
curl -X POST "https://project.canalinx.com/wp-json/platform/v1/verify-batch" \
-H "Content-Type: application/json" \
-H "X-Api-Key: pfk_9f2c4a1e0b7d68d31c5a2e4f6b8d0a1c2e3f4a5b" \
-d '{
"items": [
{ "license": "C10-0000123-LIC", "state": "CA" },
{ "license": "HEMP-0042", "state": "TX" }
]
}'Example response
{
"ok": true,
"checked": 2,
"results": [
{
"license": "C10-0000123-LIC",
"state": "CA",
"found": true,
"status": "active",
"type": "retailer",
"city": "Sacramento",
"expiry_date": "2027-01-31",
"last_synced": "2026-07-06 03:14:22"
},
{ "license": "HEMP-0042", "state": "TX", "found": false }
],
"disclaimer": "Registry mirror; confirm standing with the issuing authority for legal purposes."
}Errors
HTTP 400 {"code":"bad_input","message":"items[] of {license, state}, 1-200 per call.","data":{"status":400}}
HTTP 401 {"code":"no_key","message":"API key required (?key= or X-Api-Key).","data":{"status":401}}
HTTP 401 {"code":"bad_key","message":"Unknown or revoked API key.","data":{"status":401}}
HTTP 429 {"code":"quota","message":"Daily quota exhausted (100/day). Higher quotas come with paid tiers at launch.","data":{"status":429}}Auth & quota
- Auth: API key via
X-Api-Keyheader or?key=. Keys shorter than 20 characters are rejected outright; revoked keys return 401. - Quota accounting is per item, not per call: a 40-item call consumes 40 units of the key’s daily quota. On the free tier (100/day) that means up to 100 license checks per day — so while a single call may carry up to 200 items, a free key cannot spend more than 100 in one day.
- No partial burn: a call larger than your remaining quota is rejected with HTTP 429 and consumes nothing.
- Reset: the quota day follows the site’s local date — counters reset at midnight site time.
- Also throttled per IP: batch requests pass through the same 30-requests/hour per-IP window as single verify.
- Compact rows: batch results return license, state, found, status, type, city, expiry_date, last_synced — no county and no freshness_score. Use single
/verifywhen you need those.
Embeddable license badge
A live SVG badge that renders a license’s current registry status — embed it on any website with a plain <img> tag. No iframe, no JavaScript, hotlink-safe. The badge is honest by design: it turns green only while the state record says active.
GET https://project.canalinx.com/badge/{license}/{st}.svg
| Segment | In | Required | Notes |
|---|---|---|---|
{license} | path | yes | License number. The URL path accepts letters, digits, dot, underscore, and hyphen only; the lookup itself is punctuation-insensitive. |
{st} | path | yes | Two-letter state code, case-insensitive (lowercase in examples). The URL always ends in .svg. |
Example request
curl -o license-badge.svg "https://project.canalinx.com/badge/C10-0000123-LIC/ca.svg"Embed snippet
A pre-filled copy-paste snippet (linked to the license’s record page) is shown on every license record and in the owner dashboard. The shape:
<a href="https://YOUR-LICENSE-RECORD-PAGE">
<img src="https://project.canalinx.com/badge/C10-0000123-LIC/ca.svg"
alt="License status — verified against state registry by The Project"
width="232" height="44">
</a>Badge states
| Badge text | Dot color | When |
|---|---|---|
ACTIVE LICENSE | green (#15803d) | Registry status is exactly "active". The sub-line shows the state and the date of the last registry sync. |
EXPIRED / REVOKED / ... | amber (#b45309) | Any non-active status: the registry's own status word, uppercased. |
No registry match | grey (#6b7280) | No record for that license + state in covered registry data. |
Auth & caching
- Auth: none — hotlink-friendly by design. The SVG is 232×44 px.
- Caching: responses send
Cache-Control: public, max-age=21600(6 hours) — fresh enough to be honest, cheap enough to hotlink. - Impressions: renders are counted per license per day (a bare counter — no visitor data), and license owners see a 30-day render count on their record page and dashboard.
New-licenses RSS feed
The newest active licenses as a standard RSS 2.0 feed — pipe it into Slack (/feed add), Feedly, or any reader. Returns the 40 newest active records (those with an issue date), newest first.
GET https://project.canalinx.com/feed/new-licenses/
| Parameter | In | Required | Notes |
|---|---|---|---|
state | query | no | Two-letter state filter, e.g. NJ. Applied only when exactly two letters remain after stripping non-letters. |
type | query | no | License-type keyword in lowercase key form (e.g. retailer, cultivation); matched as a substring against the registry's license type. |
Example request
curl "https://project.canalinx.com/feed/new-licenses/?state=NJ&type=retailer"Example response (shape)
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"><channel>
<title>New Cannabis Licenses — NJ | The Project</title>
<link>https://project.canalinx.com/licenses/</link>
<description>Newest active licenses from public state registries, re-synced nightly.
Pipe into Slack (/feed add) or any reader.</description>
<item>
<title>Garden Greens LLC — Retailer (Trenton, NJ)</title>
<link>https://project.canalinx.com/licenses/nj/garden-greens-llc/</link>
<guid isPermaLink="true">https://project.canalinx.com/licenses/nj/garden-greens-llc/</guid>
<pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate>
</item>
</channel></rss>Auth & quota
- Auth: none. No API key, no signup.
- Semantics: item titles read “Business — License Type (City, ST)”; links and GUIDs point at the license record pages; pubDate comes from the registry issue date.
Query & feed API
Beyond verifying a license you already know, these keyed endpoints let you pull data: filter the registry, walk the operator graph, read the numbers, and poll the change ledger for deltas since your last sync. Same ?key= / X-Api-Key auth and daily quota as batch verification. JSON only; permitted registry fields only (no owner personal contact data is ever returned).
GET https://project.canalinx.com/wp-json/platform/v1/data/licenses
Params: state (2-letter), type, status (active = all active-class), city, page, per_page (max 100). Returns total, pages, and a results[] of permitted fields with last_synced.
GET https://project.canalinx.com/wp-json/platform/v1/data/operators
Params: q (name search), multistate (flag), page, per_page. Each result carries license_count, state_count, states[], types[], and the operator page url.
GET https://project.canalinx.com/wp-json/platform/v1/data/changes
The delta feed. Params: since (ISO date/time cursor), state, kind (new|status|renewal), per_page (max 200). Poll with the returned next_since to receive only newer changes — the point-in-time product no other registry exposes. Tracking began 2026-07-06.
GET https://project.canalinx.com/wp-json/platform/v1/data/stats
The /stats/ numbers as JSON — totals, active share, 30-day change volume, operators, cities, strains — with an as_of date.
All four require a key (create one above); unknown or missing keys return 401, exhausted quota 429. Heavier license pages cost proportionally more quota. Free tier: 100 requests/day; higher quotas arrive with paid data tiers at launch — flat fee, never a commission.
Fair use & attribution
- Attribution link required. When you display or republish data obtained from these APIs, include visible attribution with a link back to this platform — the same rule as the on-site “Cite this data” widget: data may be cited freely with attribution + link.
- No resale of raw dumps. These APIs exist for verification and integration — embedding results in your KYB checks, compliance files, and product workflows is exactly the point. Redistributing bulk extracts of the registry mirror as a dataset, or reselling API output as your own feed, is not licensed.
- Flat-fee platform. No per-record fees, no success fees, no percentage of anything — quotas above the free tier are flat-priced (“Higher quotas come with paid tiers at launch”). For higher volume or standing data needs, contact us through the intake on the Trust & Verification page.
- Carry the disclaimer. Verification responses ship with a registry-mirror disclaimer; we recommend keeping it attached wherever results reach end users.
Data sources & freshness
Every record behind these APIs comes from public state registries, re-synced nightly by our ingestion pipeline — competitor directory listings are never scraped. Rather than asking you to trust a blanket claim, each single-verify response discloses its own last_synced timestamp and freshness_score so staleness can be judged per record, and batch rows carry last_synced too. Coverage varies by state: an absent record means “not in covered data”, never “this license does not exist”. Methodology, per-state coverage, verification signals, and the dispute & contact intake all live on our Trust & Verification page. Data pages across the site carry a “Cite this data” widget — journalists and researchers may cite freely with attribution and a link, and the same attribution rule applies to anything you build on these APIs.