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

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

ParameterInRequiredNotes
licensequeryyesLicense number as issued. Matching is punctuation-insensitive (dashes, spaces, and periods are ignored); at least 3 alphanumeric characters after normalization.
statequeryyesTwo-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-After header. Batch calls pass through the same per-IP throttle, so sustained volume belongs on the keyed batch endpoint.
Every found response carries this disclaimer verbatim: “Registry mirror for informational use; confirm current standing with the issuing authority for legal purposes.” Keep it attached wherever you surface results to end users.

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

ParameterInRequiredNotes
labelbody (form or JSON)noHuman 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."
}
Keys look like 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

ParameterInRequiredNotes
itemsbody (JSON)yesArray of 1-200 objects: {"license": "...", "state": "XX"}. Same normalization rules as single verify apply per item. Each item consumes one quota unit.
X-Api-Keyheaderone of the twoYour API key. Recommended: headers keep keys out of URLs and access logs.
keyqueryone of the twoAPI 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-Key header 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 /verify when you need those.
Batch responses carry their own disclaimer verbatim: “Registry mirror; confirm standing with the issuing authority for legal purposes.”

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

SegmentInRequiredNotes
{license}pathyesLicense number. The URL path accepts letters, digits, dot, underscore, and hyphen only; the lookup itself is punctuation-insensitive.
{st}pathyesTwo-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 textDot colorWhen
ACTIVE LICENSEgreen (#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 matchgrey (#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/

ParameterInRequiredNotes
statequerynoTwo-letter state filter, e.g. NJ. Applied only when exactly two letters remain after stripping non-letters.
typequerynoLicense-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.