NPReq API v1 (1.1.0)

Download OpenAPI specification

Overview

The NPReq API gives read-only access to nurse practitioner (NP) and advanced practice registered nurse (APRN) requirements for the 50 states, DC and five territories (56 jurisdictions): practice authority, licensure, continuing education, renewal, prescribing and board details, each fact with the official source it came from.

  • Base URL: https://api.npreq.com
  • In plain words: your software asks over HTTPS (the encrypted web connection a browser uses) and gets JSON back (plain text in a fixed name-and-value layout that every programming language reads).
  • Every data endpoint is GET and returns JSON (application/json; charset=utf-8). The one POST is /v1/token, which turns your API key into a short-lived access token (see Authentication).
  • The data is the same as on npreq.com, from the same versioned build (see Data freshness).

Not for clinical or licensing decisions; verify with the state board.

Every data response repeats that sentence in its disclaimer field.

Authentication

Authentication has two steps. Your API key is long-lived and is used for one thing only: getting an access token from POST /v1/token. Every data request then sends the access token. A leaked token stops working within 6 hours on its own; the key never travels with your data traffic.

  1. Get a token (the key goes only here):
    HTTP
    POST /v1/token HTTP/1.1
    Host: api.npreq.com
    Authorization: Bearer <your API key>
    JSON
    {"access_token":"npat_live_EXAMPLE-not-a-real-token","token_type":"Bearer","expires_in":21600,"expires_at":"2026-10-27T05:30:33Z","scopes":["ce:read","meta","search","states:read"]}
  2. Call the data routes with the token:
    HTTP
    GET /v1/states HTTP/1.1
    Host: api.npreq.com
    Authorization: Bearer <your access token>

Copyable code for both steps, in Python, JavaScript, PowerShell and Bash: POST /v1/token and the endpoint pages.

The rules:

  • A token lasts 6 hours (expires_in 21600). After that the data routes answer 401 {"error":"token_expired"}: get a new token with the key and repeat the request. Renew a few minutes before expires_at so no request fails.
  • Share one token across your fleet (keep it where all your workers can read it) and renew it from one place. Each key may issue 60 tokens per hour (429 with Retry-After beyond that), and at most 10 tokens per key are live at once: issuing an 11th ends the oldest.
  • Sending the API key itself to a data route gets 401 {"error":"use_access_token"}. Sending a token to /v1/token gets 401.
  • POST /v1/token takes no query string and needs no body (a body over 1 KB is 400; a smaller one is ignored). Its reply is Cache-Control: no-store. Issuing tokens does not count against your rate limit or monthly quota.
  • A token carries the key's scopes, and every request re-checks the key: revoking the key, a suspended account, an ended subscription or a plan change takes effect on the token's next request, not when it expires.
  • Keys start npq_live_ and tokens npat_live_; they work only on api.npreq.com.
  • A key is shown once, when you create it in your npreq.com account. We store only hashes of keys and tokens, so we cannot show either again; create a new key if it is lost.
  • You can hold two active keys at a time, so you can rotate without downtime: create the new key, deploy it, then revoke the old one. Revocation takes effect on the next request, for the key and every token it issued.
  • HTTPS only. A request over plain http:// is refused with 403 and {"error":"https_required"}, and is never redirected. Whatever it carried has already crossed the network unencrypted, so a real key in it is blocked at once and you get an email asking you to rotate it; a real token in it is ended (its key keeps working). Always use https://api.npreq.com.
  • Never put a key or a token in a URL. One sent anywhere but the Authorization header (a query string, path, form body, cookie or other header) is refused with 401. A real key in the URL (the path or the query string) is also revoked immediately, because URLs end up in logs and proxies, and you get an email asking you to rotate. A real token in the URL is ended at once; its key keeps working, so just get a new token. A key or token in a form body, a cookie or another header is refused but not revoked: send it in the Authorization header instead.
  • Optional: restrict a key to a list of IP addresses or ranges in your account. Requests from other addresses get 403, at /v1/token and on the data routes. It is optional on every plan, Enterprise included.

Quick start

From nothing to your first answer in a few steps. This page uses curl and jq in a Bash shell; every endpoint page has the same in Python, JavaScript and PowerShell, with a real reply beside it, so you can judge the data before you buy.

  1. See the data first. /v1/meta is public: no account, key or token. It says which data release the API serves:
    curl (Bash)
    curl -s https://api.npreq.com/v1/meta
    JSON
    {
      "data_version": "2026.11.5",
      "last_updated": "2026-11-02T23:52:43Z",
      "jurisdiction_count": 56
    }
  2. Choose a plan and create your account. Choose a plan: Starter and Agency are bought online, and your account is created with your email address on the way to checkout. Enterprise is arranged with us.
  3. Create an API key. On your account page, choose Create a key. The key is shown only once, so store it somewhere safe. The examples read it from the environment variable NPREQ_API_KEY:
    curl (Bash)
    export NPREQ_API_KEY='paste your key here'
  4. Get an access token. The key is used for one thing only: getting a token, which lasts 6 hours.
    curl (Bash)
    TOKEN=$(curl -s --fail -X POST https://api.npreq.com/v1/token -H "Authorization: Bearer $NPREQ_API_KEY" | jq -r .access_token)
  5. Make your first request. Ask for the list of jurisdictions, sending the token:
    curl (Bash)
    curl -s --fail https://api.npreq.com/v1/states -H "Authorization: Bearer $TOKEN"

The reply:

Shortened for this page: data shows 3 of 56 jurisdictions. Everything else is the whole reply.

JSON
{
  "meta": {
    "data_version": "2026.11.5",
    "generated_at": "2026-10-18T13:32:19.920846Z",
    "trace": "dc47d5852b376c6c"
  },
  "disclaimer": "Not for clinical or licensing decisions; verify with the state board.",
  "data": [
    {
      "code": "AK",
      "name": "Alaska",
      "practice_authority": "Full",
      "has_data": true
    },
    {
      "code": "AL",
      "name": "Alabama",
      "practice_authority": "Collaborative",
      "has_data": true
    },
    {
      "code": "AR",
      "name": "Arkansas",
      "practice_authority": "Transitional",
      "has_data": true
    }
  ]
}

Next: each endpoint page lists its parameters and every field of its reply, and Examples has a complete client in four languages. Authentication explains tokens and when to renew them.

Endpoints

Each endpoint has its own page: its parameters, a real response and every field in it, its own errors, and examples to copy.

EndpointScopeReturns
POST /v1/tokennone (API key)a 6-hour access token for the data routes
GET /v1/metanone (public)data version, last update, jurisdiction count
GET /v1/statesstates:readthe 56 jurisdictions: code, name, practice authority, has data
GET /v1/states/{code}states:readthe full record for one jurisdiction, with sources
GET /v1/states/{code}/cece:readcontinuing-education requirements for one jurisdiction
GET /v1/search?q=searchup to 20 matching facts
GET /healthnone{"status":"ok"}

{code} is the two-letter postal code in capitals (CA, DC, PR, GU). Lower case or an unknown code is 404.

The examples come in four languages: Python (with requests), JavaScript (Node 18 or later, its built-in fetch; save the code as a .mjs file and run node file.mjs), PowerShell 7 and Bash (with curl and jq). Each one gets its own token first, so every example runs on its own once NPREQ_API_KEY is set (see Quick start). The Output block under the examples shows what each of them prints.

Response shape

Every data response (every route except /v1/meta, /v1/token and /health) is:

JSON
{
  "meta": {
    "data_version": "2026.11.5",
    "generated_at": "2026-10-18T13:32:19.920846Z",
    "trace": "dc47d5852b376c6c"
  },
  "disclaimer": "Not for clinical or licensing decisions; verify with the state board.",
  "data": {}
}
FieldTypeAlways present?Meaning
meta.data_versionstringYesThe data release the reply comes from (see Data freshness).
meta.generated_atstringYesWhen that release was built, UTC (ISO 8601).
meta.tracestringYesHelps us find your requests in support. Quote it, with the X-Request-Id header, when you contact us.
disclaimerstringYes"Not for clinical or licensing decisions; verify with the state board." Show it with the data.
dataobject or arrayYesThe answer itself: each endpoint page describes it.

POST /v1/token

Get an access token

Turns your API key into an access token for the data routes. This is the only request that carries the key. Tokens do not count against your rate limit or monthly quota; each key may issue 60 tokens per hour, and at most 10 are live at once (an 11th ends the oldest).

It takes no query string and needs no body: a body over 1 KB is 400, a smaller one is ignored. The scope is none.

The reply is Cache-Control: no-store. Renew a few minutes before expires_at, and share one token across your workers (see Authentication).

Authorizations

apiKey HTTP bearer: Your API key (it starts npq_live_), used only at POST /v1/token. A key in a URL is revoked at once; sent to a data route it is refused with use_access_token.

Parameters

Header

NameTypeRequiredDescriptionRulesExample
AuthorizationstringYesBearer, a space and your API key. This is the only request that sends the key.-Bearer $NPREQ_API_KEY

Responses

200 The token. Cache-Control: no-store.

Response schema: TokenResponse

FieldTypeAlways present?Meaning
access_tokenstringYesThe token to send as 'Authorization: Bearer <token>' on the data routes. It starts npat_live_.
token_typestringYesAlways Bearer. One of: Bearer.
expires_inintegerYesSeconds the token lasts: 21600, 6 hours.
expires_atstringYesWhen it stops working, UTC.
scopesarray of stringYesWhat the token may read: the key's scopes, sorted.

Errors

400 A query string, or a body over 1 KB.

Body: {"error":"bad_request"}

401 A missing, unknown, revoked or suspended key, or a token sent instead of a key.

Body: {"error":"unauthorized"}

It can also answer with the errors every route shares: 403 405 414 429 500 503. They are on the Errors page.

Request samples

curl (Bash)
T=$(curl -s --fail -X POST https://api.npreq.com/v1/token -H "Authorization: Bearer $NPREQ_API_KEY")
TOKEN=$(jq -r .access_token <<<"$T")
jq -r '"\(.token_type) token for \(.expires_in / 3600) hours: \(.scopes | join(" "))"' <<<"$T"
Python
import os, requests

r = requests.post("https://api.npreq.com/v1/token", timeout=30,
                  headers={"Authorization": f"Bearer {os.environ['NPREQ_API_KEY']}"})
r.raise_for_status()
t = r.json()
token = t["access_token"]                    # send this on the data routes
print(t["token_type"], "token for", t["expires_in"] // 3600, "hours:", " ".join(t["scopes"]))
JavaScript
const r = await fetch("https://api.npreq.com/v1/token", {
  method: "POST", headers: { Authorization: `Bearer ${process.env.NPREQ_API_KEY}` },
});
if (!r.ok) throw new Error(`NPReq token ${r.status}`);
const t = await r.json();
const token = t.access_token;                // send this on the data routes
console.log(t.token_type, "token for", t.expires_in / 3600, "hours:", t.scopes.join(" "));
PowerShell
$t = Invoke-RestMethod -Method Post -Uri "https://api.npreq.com/v1/token" -Headers @{ Authorization = "Bearer $env:NPREQ_API_KEY" }
$token = $t.access_token
"$($t.token_type) token for $($t.expires_in / 3600) hours: $($t.scopes -join ' ')"
Output
Bearer token for 6 hours: ce:read meta search states:read

Response samples

200 A token for a key with every scope
{
  "access_token": "npat_live_EXAMPLE-not-a-real-token",
  "token_type": "Bearer",
  "expires_in": 21600,
  "expires_at": "2026-10-27T05:30:33Z",
  "scopes": [
    "ce:read",
    "meta",
    "search",
    "states:read"
  ]
}
400 Error: bad_request
{
  "error": "bad_request"
}
401 Error: unauthorized
{
  "error": "unauthorized"
}

GET /v1/meta

Data version and coverage

Public: it needs no key or token. A key or token in the Authorization header is ignored. One anywhere else is refused with 401 as on every route, and a real one in the URL is also blocked (see Authentication). The reply is the same for every caller and is the bare object, with no meta or disclaimer envelope. It takes no parameters, and any query string is 400.

  • It has its own limit of 30 requests a minute per IP (an IPv6 /64 counts as one IP), and an overall limit for all callers together. Beyond it you get 429 with Retry-After. It counts against no monthly quota and sends no X-RateLimit or X-Quota headers.
  • It is Cache-Control: public, max-age=300, with an ETag and Last-Modified: If-None-Match gives 304.

Cheap to call: poll it (for example daily) and refetch states only when data_version changes. HEAD /v1/meta (and HEAD /health) answers like GET with the same headers and no body, for monitors and If-None-Match checks. The data routes take GET only.

Authorizations

None: this endpoint is public, with no key or token.

Parameters

NameTypeRequiredDescriptionRulesExample
None--It takes no parameters, key or token.--

Responses

200 The metadata.

Response schema: PublicMeta

FieldTypeAlways present?Meaning
data_versionstringYesThe data release the API serves now. It changes when state data changes.
last_updatedstringYes, may be nullWhen that release's newest update was applied, UTC (ISO 8601).
jurisdiction_countintegerYesHow many jurisdictions the API serves: 56.
304 Not modified: the ETag you sent still matches. No body.

No body.

Errors

400 Any query string.

Body: {"error":"bad_request"}

401 A key or token outside the Authorization header.

Body: {"error":"unauthorized"}

403 A plain http:// request.

Body: {"error":"https_required"}

It can also answer with the errors every route shares: 405 414 429 500 503. They are on the Errors page.

Request samples

curl (Bash)
curl -s --fail https://api.npreq.com/v1/meta | jq -r '"Data version \(.data_version), \(.jurisdiction_count) jurisdictions"'
Python
import requests

r = requests.get("https://api.npreq.com/v1/meta", timeout=30)
r.raise_for_status()
m = r.json()
print(f"Data version {m['data_version']}, {m['jurisdiction_count']} jurisdictions")
JavaScript
const r = await fetch("https://api.npreq.com/v1/meta");
if (!r.ok) throw new Error(`NPReq ${r.status}`);
const m = await r.json();
console.log(`Data version ${m.data_version}, ${m.jurisdiction_count} jurisdictions`);
PowerShell
$m = Invoke-RestMethod -Uri "https://api.npreq.com/v1/meta"
"Data version $($m.data_version), $($m.jurisdiction_count) jurisdictions"
Output
Data version 2026.11.5, 56 jurisdictions

Response samples

200 The public metadata
{
  "data_version": "2026.11.5",
  "last_updated": "2026-11-02T23:52:43Z",
  "jurisdiction_count": 56
}
400 Error: bad_request
{
  "error": "bad_request"
}
401 Error: unauthorized
{
  "error": "unauthorized"
}
403 Error: https_required
{
  "error": "https_required"
}

GET /v1/states

List all jurisdictions

The 56 jurisdictions, one entry each, ordered by code.

  • Authentication: an access token. Scope: states:read.

It takes no query parameters: any query parameter is 400.

Authorizations

accessToken HTTP bearer: An access token from POST /v1/token (it starts npat_live_). Send it in the Authorization header only: one anywhere else is refused, and one in a URL is ended at once.

Parameters

Header

NameTypeRequiredDescriptionRulesExample
AuthorizationstringYesBearer, a space and your access token from POST /v1/token.-Bearer $TOKEN

Responses

200 The data, in the meta and disclaimer envelope.

Response schema: StatesResponse

FieldTypeAlways present?Meaning
metaResponseMetaYes
meta.data_versionstringYesThe data release the reply comes from.
meta.generated_atstringYes, may be nullWhen that release was built, UTC (ISO 8601).
meta.tracestringYesHelps us find your requests in support. Quote it, with the X-Request-Id header.
disclaimerstringYesShow it with the data. One of: Not for clinical or licensing decisions; verify with the state board..
dataarray of JurisdictionSummaryYes
data[].codestringYesThe two-letter postal code, in capitals. Use it in /v1/states/{code}.
data[].namestringYesThe jurisdiction's name.
data[].practice_authoritystringYes, may be nullNP practice authority in a word or two, for example Full, Reduced, Restricted, Collaborative or Transitional.
data[].has_databooleanYestrue when the jurisdiction has a sourced record.
304 Not modified: the ETag you sent still matches. No body. It counts against the rate limit, not the monthly quota.

No body.

Errors

400 Any query parameter the route does not take, a repeated one, or array syntax.

Body: {"error":"bad_request"}

It can also answer with the errors every route shares: 304 401 403 405 414 429 500 503. They are on the Errors page.

Request samples

curl (Bash)
TOKEN=$(curl -s --fail -X POST https://api.npreq.com/v1/token -H "Authorization: Bearer $NPREQ_API_KEY" | jq -r .access_token)
curl -s --fail https://api.npreq.com/v1/states -H "Authorization: Bearer $TOKEN" \
    | jq -r '"\(.data | length) jurisdictions", (.data[:3][] | "\(.code) \(.name) \(.practice_authority)")'
Python
import os, requests

API = "https://api.npreq.com"
t = requests.post(f"{API}/v1/token", timeout=30, headers={"Authorization": f"Bearer {os.environ['NPREQ_API_KEY']}"})
t.raise_for_status()
headers = {"Authorization": f"Bearer {t.json()['access_token']}"}

r = requests.get(f"{API}/v1/states", headers=headers, timeout=30)
r.raise_for_status()
states = r.json()["data"]
print(len(states), "jurisdictions")
for s in states[:3]:
    print(s["code"], s["name"], s["practice_authority"])
JavaScript
const API = "https://api.npreq.com";
const t = await fetch(`${API}/v1/token`, { method: "POST", headers: { Authorization: `Bearer ${process.env.NPREQ_API_KEY}` } });
if (!t.ok) throw new Error(`NPReq token ${t.status}`);
const headers = { Authorization: `Bearer ${(await t.json()).access_token}` };

const r = await fetch(`${API}/v1/states`, { headers });
if (!r.ok) throw new Error(`NPReq ${r.status}`);
const states = (await r.json()).data;
console.log(states.length, "jurisdictions");
for (const s of states.slice(0, 3)) console.log(s.code, s.name, s.practice_authority);
PowerShell
$Api = "https://api.npreq.com"
$token = (Invoke-RestMethod -Method Post -Uri "$Api/v1/token" -Headers @{ Authorization = "Bearer $env:NPREQ_API_KEY" }).access_token
$r = Invoke-RestMethod -Uri "$Api/v1/states" -Headers @{ Authorization = "Bearer $token" }
"$($r.data.Count) jurisdictions"
$r.data | Select-Object -First 3 | ForEach-Object { "$($_.code) $($_.name) $($_.practice_authority)" }
Output
56 jurisdictions
AK Alaska Full
AL Alabama Collaborative
AR Arkansas Transitional

Response samples

200 The jurisdictions (shortened: data 3 of 56)
{
  "meta": {
    "data_version": "2026.11.5",
    "generated_at": "2026-10-18T13:32:19.920846Z",
    "trace": "dc47d5852b376c6c"
  },
  "disclaimer": "Not for clinical or licensing decisions; verify with the state board.",
  "data": [
    {
      "code": "AK",
      "name": "Alaska",
      "practice_authority": "Full",
      "has_data": true
    },
    {
      "code": "AL",
      "name": "Alabama",
      "practice_authority": "Collaborative",
      "has_data": true
    },
    {
      "code": "AR",
      "name": "Arkansas",
      "practice_authority": "Transitional",
      "has_data": true
    }
  ]
}
400 Error: bad_request
{
  "error": "bad_request"
}

GET /v1/states/{code}

Get one jurisdiction

One jurisdiction's reader record: summary fields, then sections (overview, licensure, ce_rules, renewal_detail, prescribing_detail, board), each fact as {label, value, source}, plus a sources list of the official pages used. Text is plain reader text: no statute citations inside requirement text and no raw codes. The source URL for each fact carries the legal reference.

Some renewal_detail facts are Yes or No flags, each cited to its own official source when one is recorded: CE required at renewal, Practice hours at renewal, and Active national certification required at renewal (whether renewing the APRN licence needs a current national certification). A practice-hour rule adds Practice hours (<licence type>), or Practice hours that change CE (<licence type>) where the rule changes the CE owed rather than renewal, with If practice hours are met and If practice hours are not met facts.

  • Authentication: an access token. Scope: states:read.

It takes no query parameters: any query parameter is 400.

Authorizations

accessToken HTTP bearer: An access token from POST /v1/token (it starts npat_live_). Send it in the Authorization header only: one anywhere else is refused, and one in a URL is ended at once.

Parameters

Path parameters

NameTypeRequiredDescriptionRulesExample
codestringYesTwo capital letters: one of the codes GET /v1/states lists. Lower case or an unknown code is 404.pattern ^[A-Z]{2}$TX

Header

NameTypeRequiredDescriptionRulesExample
AuthorizationstringYesBearer, a space and your access token from POST /v1/token.-Bearer $TOKEN

Responses

200 The data, in the meta and disclaimer envelope.

Response schema: StateResponse

FieldTypeAlways present?Meaning
metaResponseMetaYes
meta.data_versionstringYesThe data release the reply comes from.
meta.generated_atstringYes, may be nullWhen that release was built, UTC (ISO 8601).
meta.tracestringYesHelps us find your requests in support. Quote it, with the X-Request-Id header.
disclaimerstringYesShow it with the data. One of: Not for clinical or licensing decisions; verify with the state board..
dataStateRecordYesOne jurisdiction's reader record. New fields may be added (see Versioning).
data.codestringYesThe two-letter postal code.
data.namestringYesThe jurisdiction's name.
data.practice_authoritystringYesNP practice authority in a word or two, as in /v1/states.
data.practice_notestringYesWhat that practice authority means there, in plain words.
data.license_typesarray of stringYesThe NP licence types the record covers.
data.ce_hoursnumberYes, may be nullTotal CE hours per renewal cycle; null when the jurisdiction sets no single total.
data.ce_cycle_yearsintegerYesLength of the renewal cycle, in years.
data.ce_notenullYesReserved; null today.
data.renewalstringYesWhen the licence renews, in words.
data.key_rulesarray of anyYesReserved; empty today.
data.last_updatedstringYesThe date (YYYY-MM-DD) the record last changed.
data.last_verifiedstringYesThe date the record was last checked against its sources.
data.source_notestringYesHow the record was built, in words.
data.board_namestringYesThe board that licenses NPs there.
data.prescribingintegerYes, may be null1 when NPs there have prescriptive authority; null when not recorded.
data.has_databooleanYestrue when the record is sourced.
data.data_qualitystringYesceme for a sourced record.
data.detail_layoutstringYesAlways sections. One of: sections.
data.overviewobjectYesPractice authority, compact membership, scope and telehealth.
data.overview.practice_authoritystringYesThe same as practice_authority.
data.overview.factsarray of FactYes
data.overview.facts[].labelstringYesWhat the fact is, for example 'Renewal fee'.
data.overview.facts[].valuestring or integerYesThe fact, in plain words or as a number.
data.overview.facts[].sourcestringNoThe official page the fact came from. Left out when the fact has no page of its own; the record's sources still list where it came from.
data.overview.alertsarray of AlertYes
data.overview.alerts[].titlestringYesThe notice.
data.overview.alerts[].descriptionstringYesThe notice in full.
data.overview.alerts[].severitystringYesThe board data's own word for how serious it is, such as HIGH or info: compare it without regard to case. For software, use level.
data.overview.alerts[].levelstringNoHow serious it is, always one of these four words in lower case. It reads severity: MODERATE and warning are medium, IMPORTANT is high. Added in 1.1.0: a dataset built before then has no level, so handle a reply without it. One of: info, medium, high, critical.
data.overview.alerts[].effective_datestringYes, may be nullThe date (YYYY-MM-DD) it takes effect, when known.
data.overview.alerts[].source_urlstringYes, may be nullThe official page for it.
data.licensureobjectYesInitial licensure.
data.licensure.license_typestringYesThe licence type the section describes.
data.licensure.factsarray of FactYes
data.licensure.feesarray of FeeYes
data.licensure.fees[].labelstringYesWhat the fee is for.
data.licensure.fees[].amountnumberYesUS dollars.
data.licensure.fees[].sourcestringYes, may be nullThe official page for it.
data.ce_rulesarray of CeRuleYesThe same objects as GET /v1/states/{code}/ce.
data.ce_rules[].topicstringYesThe subject, for example General CE, Pharmacology or Implicit Bias.
data.ce_rules[].titlestringYesThe topic and who the rule is for, as one line.
data.ce_rules[].hoursnumberYes, may be nullHours the rule asks for; null when it sets none.
data.ce_rules[].hours_labelstringYes, may be nullThe hours in words, with the period when there is one.
data.ce_rules[].recurrencestringYesHow often it is owed: every_renewal, first_cycle (the first renewal after licensure), one_time, conditional (only for some licensees: see required_for), periodic (on its own schedule: see description), none (no CE needed), note (information, no hours) or expired (no longer owed). New values may be added: handle one you do not know.
data.ce_rules[].cycle_monthsintegerYes, may be nullThe period the hours cover, in months, when the rule names one.
data.ce_rules[].required_forstringYes, may be nullWho the rule applies to, in words.
data.ce_rules[].descriptionstringYes, may be nullThe rule in plain words.
data.ce_rules[].applicabilitynullYesReserved; null today.
data.ce_rules[].alternative_pathwaystringYes, may be nullAnother way to meet the rule, when there is one.
data.ce_rules[].effective_fromstringYes, may be nullThe date (YYYY-MM-DD) the rule starts, when known.
data.ce_rules[].effective_untilstringYes, may be nullThe date it ends, when it has an end.
data.ce_rules[].source_urlstringYes, may be nullThe official page for the rule.
data.ce_rules[].source_urlsarray of stringYesEvery official page for the rule (the first is source_url).
data.ce_rules[].last_verifiedstringYes, may be nullThe date the rule was last checked against its source.
data.ce_rules[].notesstringYes, may be nullExtra conditions, such as accreditation.
data.ce_rules[].rule_keystringYesA stable name for the rule: use it to track a rule between data versions.
data.ce_rules[].statusstringNoexpired when the rule is no longer owed; left out otherwise. One of: expired.
data.renewal_detailobjectYesRenewal period, deadline, fees and process.
data.renewal_detail.summarystringYesWhen the licence renews, in words.
data.renewal_detail.factsarray of FactYes
data.prescribing_detailobjectYesPrescriptive authority, schedules, DEA and coursework.
data.prescribing_detail.summarystringYesThe prescribing rules in a few sentences.
data.prescribing_detail.factsarray of FactYes
data.boardBoardYesThe board that licenses NPs. Any field but name, url and portal may be null.
data.board.phonestringYes, may be nullPhone.
data.board.emailstringYes, may be nullEmail.
data.board.faxstringYes, may be nullFax.
data.board.addressstringYes, may be nullStreet address.
data.board.citystringYes, may be nullCity.
data.board.zipstringYes, may be nullZIP code.
data.board.mailing_addressstringYes, may be nullMailing address.
data.board.physical_addressstringYes, may be nullPhysical address.
data.board.office_hoursstringYes, may be nullOffice hours.
data.board.contact_pagestringYes, may be nullThe board's contact page.
data.board.agencystringYes, may be nullReserved; null today.
data.board.namestringYesThe board's name.
data.board.urlstringYesThe board's website.
data.board.portalstringYesThe board's licensing portal.
data.board.sourcesobjectYesThe page each contact detail came from, by field name.
data.sourcesarray of SourceYesThe official pages the record was built from.
data.sources[].labelstringYesThe page, in a few words.
data.sources[].urlstringYesThe page.
data.quick_linksarray of QuickLinkYesUseful board pages.
data.quick_links[].titlestringYesThe page title.
data.quick_links[].urlstringYesThe page.
data.quick_links[].categorystringYesFor example portal, renewal or fees.
data.quick_links[].descriptionstringYes, may be nullWhat the page is for.
data.quick_links[].is_officialintegerYes1 for the board's own site, 0 otherwise. One of: 0, 1.
304 Not modified: the ETag you sent still matches. No body. It counts against the rate limit, not the monthly quota.

No body.

Errors

400 Any query parameter the route does not take, a repeated one, or array syntax.

Body: {"error":"bad_request"}

It can also answer with the errors every route shares: 304 401 403 404 405 414 429 500 503. They are on the Errors page.

Request samples

curl (Bash)
TOKEN=$(curl -s --fail -X POST https://api.npreq.com/v1/token -H "Authorization: Bearer $NPREQ_API_KEY" | jq -r .access_token)
curl -s --fail https://api.npreq.com/v1/states/TX -H "Authorization: Bearer $TOKEN" | jq -r '.data
    | "\(.name): \(.practice_authority) practice authority", "Renewal: \(.renewal_detail.summary)",
      "CE: \(.ce_hours) hours every \(.ce_cycle_years) years, \(.ce_rules | length) rules", "\(.sources | length) sources"'
Python
import os, requests

API = "https://api.npreq.com"
t = requests.post(f"{API}/v1/token", timeout=30, headers={"Authorization": f"Bearer {os.environ['NPREQ_API_KEY']}"})
t.raise_for_status()
headers = {"Authorization": f"Bearer {t.json()['access_token']}"}

r = requests.get(f"{API}/v1/states/TX", headers=headers, timeout=30)
r.raise_for_status()
tx = r.json()["data"]
print(f"{tx['name']}: {tx['practice_authority']} practice authority")
print("Renewal:", tx["renewal_detail"]["summary"])
print(f"CE: {tx['ce_hours']} hours every {tx['ce_cycle_years']} years, {len(tx['ce_rules'])} rules")
print(len(tx["sources"]), "sources")
JavaScript
const API = "https://api.npreq.com";
const t = await fetch(`${API}/v1/token`, { method: "POST", headers: { Authorization: `Bearer ${process.env.NPREQ_API_KEY}` } });
if (!t.ok) throw new Error(`NPReq token ${t.status}`);
const headers = { Authorization: `Bearer ${(await t.json()).access_token}` };

const r = await fetch(`${API}/v1/states/TX`, { headers });
if (!r.ok) throw new Error(`NPReq ${r.status}`);
const tx = (await r.json()).data;
console.log(`${tx.name}: ${tx.practice_authority} practice authority`);
console.log("Renewal:", tx.renewal_detail.summary);
console.log(`CE: ${tx.ce_hours} hours every ${tx.ce_cycle_years} years, ${tx.ce_rules.length} rules`);
console.log(tx.sources.length, "sources");
PowerShell
$Api = "https://api.npreq.com"
$token = (Invoke-RestMethod -Method Post -Uri "$Api/v1/token" -Headers @{ Authorization = "Bearer $env:NPREQ_API_KEY" }).access_token
$tx = (Invoke-RestMethod -Uri "$Api/v1/states/TX" -Headers @{ Authorization = "Bearer $token" }).data
"$($tx.name): $($tx.practice_authority) practice authority"
"Renewal: $($tx.renewal_detail.summary)"
"CE: $($tx.ce_hours) hours every $($tx.ce_cycle_years) years, $($tx.ce_rules.Count) rules"
"$($tx.sources.Count) sources"
Output
Texas: Restricted practice authority
Renewal: Last day of birth month every 2 years
CE: 20 hours every 2 years, 13 rules
29 sources

Response samples

200 Texas (shortened: overview.facts 3 of 18, overview.alerts 1 of 4, licensure.facts 3 of 9, licensure.fees 2 of 4, ce_rules 1 of 13, renewal_detail.facts 3 of 13, prescribing_detail.facts 2 of 7, sources 3 of 29, quick_links 2 of 18)
{
  "meta": {
    "data_version": "2026.11.5",
    "generated_at": "2026-10-30T19:31:18.508461Z",
    "trace": "c1076a1303121188"
  },
  "disclaimer": "Not for clinical or licensing decisions; verify with the state board.",
  "data": {
    "code": "TX",
    "name": "Texas",
    "practice_authority": "Restricted",
    "practice_note": "A prescriptive authority agreement must describe a quality assurance and improvement plan that includes chart review, with the number of charts to be reviewed decided by the physician and the APRN, and periodic meetings between the APRN and the physician. The meetings must include sharing information about patient treatment and care, needed changes in patient care plans and referral issues, and discussion of patient care improvement. They must be documented and take place at least once a month in a manner the physician and the APRN decide. These requirements, as amended effective September 1, 2019, apply only to agreements entered into on or after that date. An agreement entered into before September 1, 2019 is governed by the law in effect when it was entered into, which called for periodic face-to-face meetings.",
    "license_types": [
      "APRN"
    ],
    "ce_hours": 20,
    "ce_cycle_years": 2,
    "ce_note": null,
    "renewal": "Last day of birth month every 2 years",
    "key_rules": [],
    "last_updated": "2026-09-02",
    "last_verified": "2026-09-01",
    "source_note": "Assembled from the NPReq reference data, pack 2026.11.5; see the listed sources for each fact.",
    "board_name": "Texas Board of Nursing",
    "prescribing": 1,
    "has_data": true,
    "data_quality": "ceme",
    "detail_layout": "sections",
    "overview": {
      "practice_authority": "Restricted",
      "facts": [
        {
          "label": "Practice authority",
          "value": "Restricted",
          "source": "https://www.bon.texas.gov/faq_practice_aprn.asp.html"
        },
        {
          "label": "Details",
          "value": "A prescriptive authority agreement must describe a quality assurance and improvement plan that includes chart review, with the number of charts to be reviewed decided by the physician and the APRN, and periodic meetings between the APRN and the physician. The meetings must include sharing information about patient treatment and care, needed changes in patient care plans and referral issues, and discussion of patient care improvement. They must be documented and take place at least once a month in a manner the physician and the APRN decide. These requirements, as amended effective September 1, 2019, apply only to agreements entered into on or after that date. An agreement entered into before September 1, 2019 is governed by the law in effect when it was entered into, which called for periodic face-to-face meetings.",
          "source": "https://www.bon.texas.gov/faq_practice_aprn.asp.html"
        },
        {
          "label": "Collaboration",
          "value": "Required. Texas nurse practitioners may not provide any medical aspects of care independently; there must be a collaborative relationship and practice agreement with a physician, and when providing medical aspects of care the nurse practitioner must use written mechanisms that provide authority for that care. Prescriptive authority is exercised under one of two delegation mechanisms: a prescriptive authority agreement or, in a hospital or long term care facility-based practice, facility-based protocols."
        }
      ],
      "alerts": [
        {
          "title": "Texas is NOT a Full Practice Authority State",
          "description": "Texas APRNs need delegated authority from a licensed physician to provide medical aspects of patient care, and a prescriptive authority agreement must be in place with a physician who delegates prescriptive authority. For agreements entered into on or after September 1, 2019, the periodic meetings must take place at least once a month, in a manner the parties decide; face-to-face meetings are no longer strictly required. The number of charts to review is set by the parties: the law gives no specific number or percentage. For agreements entered into before September 1, 2019, the type of license and the time spent practicing under the agreement determined how often and how the parties had to meet. Physicians must register every APRN they supervise with the Texas Medical Board before the APRN begins working for them, and must notify the board within 30 days of any change to the scope of the delegation.",
          "severity": "CRITICAL",
          "level": "critical",
          "effective_date": "2019-09-01",
          "source_url": "https://www.tmb.texas.gov/apply-renew/physician/prescribing-and-supervision"
        }
      ]
    },
    "licensure": {
      "license_type": "APRN",
      "facts": [
        {
          "label": "License type",
          "value": "APRN",
          "source": "https://www.bon.texas.gov/applications_advanced_practice_registered_nurse.asp.html"
        },
        {
          "label": "RN license required",
          "value": "Yes"
        },
        {
          "label": "Graduate degree required",
          "value": "Master's or Doctoral degree in nursing"
        }
      ],
      "fees": [
        {
          "label": "Initial application",
          "amount": 150,
          "source": null
        },
        {
          "label": "Renewal",
          "amount": 68,
          "source": null
        }
      ]
    },
    "ce_rules": [
      {
        "topic": "General CE",
        "title": "General CE — Every practitioner who holds the licence",
        "hours": 20,
        "hours_label": "20 hours / 24 months",
        "recurrence": "every_renewal",
        "cycle_months": 24,
        "required_for": "Every practitioner who holds the licence",
        "description": "Each licensing period, nurses must complete 20 contact hours of continuing education in their area of practice, or achieve, maintain or renew a board-approved national nursing certification in their area of practice, and meet any targeted continuing education requirements that apply. For APRNs, the 20 hours must be appropriate to the APRN role and population focus, and they also count for the RN license. The Board waives most continuing education at a nurse's first timely renewal after initial Texas licensure, but its pages do not agree on whether targeted requirements are waived too, and the waiver follows your first Texas nursing license rather than your APRN license. For that reason the 20 hours and every targeted requirement that applies to you stay on your list at every renewal, including the first; completing them keeps you compliant under each reading.",
        "applicability": null,
        "alternative_pathway": null,
        "effective_from": null,
        "effective_until": null,
        "source_url": "https://www.bon.texas.gov/education_continuing_education.asp.html",
        "source_urls": [
          "https://www.bon.texas.gov/education_continuing_education.asp.html"
        ],
        "last_verified": "2026-02-02",
        "notes": null,
        "rule_key": "TX-general_ce"
      }
    ],
    "renewal_detail": {
      "summary": "Last day of birth month every 2 years",
      "facts": [
        {
          "label": "Period",
          "value": "24 months",
          "source": "https://www.bon.texas.gov/licensure_renewal.asp.html"
        },
        {
          "label": "Window opens",
          "value": "60 days before expiration",
          "source": "https://www.bon.texas.gov/forms_fees.asp.html"
        },
        {
          "label": "Start",
          "value": "Timely renewal application visible in Texas Nurse Portal only if expiration is within 60 days.",
          "source": "https://www.bon.texas.gov/forms_fees.asp.html"
        }
      ]
    },
    "prescribing_detail": {
      "summary": "A Texas APRN orders and prescribes drugs and devices under authority delegated by a physician. The APRN needs full APRN licensure from the Board of Nursing and a valid prescription authorization number, and may order or prescribe only drugs or devices authorized by a prescriptive authority agreement or, in a facility-based practice, by a prescriptive authority agreement or by protocols or other written authorization. An APRN with full licensure and a valid prescription authorization number may obtain authority to order and prescribe certain categories of controlled substances, and must comply with all federal and state laws on controlled substances, including the requirements of the Texas Department of Public Safety and the United States Drug Enforcement Administration (DEA). Each prescription for a controlled substance must show the DEA numbers of both the APRN and the delegating physician. Controlled substances in Schedules III through V are limited to a 90-day supply, including refills; a refill beyond the first 90 days, or a prescription for a child under two years old, requires a consultation with the delegating physician, noted in the patient's chart. Schedule II controlled substances may be ordered or prescribed only in a hospital facility-based practice, under policies approved by the hospital's medical staff or a committee of the medical staff as the hospital's bylaws provide, for a patient admitted to the hospital for an intended stay of 24 hours or more or receiving services in the hospital's emergency department; or as part of the plan of care for a patient who has executed a written certification of a terminal illness, has elected hospice care and is receiving hospice treatment from a qualified hospice provider.",
      "facts": [
        {
          "label": "Prescriptive authority",
          "value": "Yes",
          "source": "https://www.bon.texas.gov/faq_practice_aprn.asp.html"
        },
        {
          "label": "Details",
          "value": "A Texas APRN orders and prescribes drugs and devices under authority delegated by a physician. The APRN needs full APRN licensure from the Board of Nursing and a valid prescription authorization number, and may order or prescribe only drugs or devices authorized by a prescriptive authority agreement or, in a facility-based practice, by a prescriptive authority agreement or by protocols or other written authorization. An APRN with full licensure and a valid prescription authorization number may obtain authority to order and prescribe certain categories of controlled substances, and must comply with all federal and state laws on controlled substances, including the requirements of the Texas Department of Public Safety and the United States Drug Enforcement Administration (DEA). Each prescription for a controlled substance must show the DEA numbers of both the APRN and the delegating physician. Controlled substances in Schedules III through V are limited to a 90-day supply, including refills; a refill beyond the first 90 days, or a prescription for a child under two years old, requires a consultation with the delegating physician, noted in the patient's chart. Schedule II controlled substances may be ordered or prescribed only in a hospital facility-based practice, under policies approved by the hospital's medical staff or a committee of the medical staff as the hospital's bylaws provide, for a patient admitted to the hospital for an intended stay of 24 hours or more or receiving services in the hospital's emergency department; or as part of the plan of care for a patient who has executed a written certification of a terminal illness, has elected hospice care and is receiving hospice treatment from a qualified hospice provider.",
          "source": "https://www.bon.texas.gov/faq_practice_aprn.asp.html"
        }
      ]
    },
    "board": {
      "name": "Texas Board of Nursing",
      "phone": "(512) 305-7400",
      "email": "aprn@bon.texas.gov",
      "fax": "(512) 305-7401",
      "address": "1801 Congress Avenue, Suite 10-200",
      "city": "Austin",
      "zip": "78701",
      "mailing_address": "1801 Congress Avenue, Suite 10-200, Austin, TX 78701",
      "physical_address": "1801 Congress Avenue, Suite 10-200, Austin, TX 78701",
      "office_hours": "8:00 am to 5:00 pm",
      "url": "https://www.bon.texas.gov/",
      "contact_page": null,
      "portal": "https://www.bon.texas.gov",
      "agency": null,
      "sources": {
        "phone": "https://www.bon.texas.gov/contact_us.asp.html",
        "email": "https://www.bon.texas.gov/contact_us.asp.html",
        "address": "https://www.bon.texas.gov/contact_us.asp.html",
        "board": "https://www.bon.texas.gov/"
      }
    },
    "sources": [
      {
        "label": "bon.state.tx.us",
        "url": "http://www.bon.state.tx.us/"
      },
      {
        "label": "Laws and rules nursing practice act.asp",
        "url": "http://www.bon.state.tx.us/laws_and_rules_nursing_practice_act.asp.html"
      },
      {
        "label": "Licensure renewal.asp",
        "url": "http://www.bon.state.tx.us/licensure_renewal.asp.html"
      }
    ],
    "quick_links": [
      {
        "title": "Texas Board of Nursing",
        "url": "https://www.bon.texas.gov",
        "category": "portal",
        "description": "Official TX BON website and services",
        "is_official": 1
      },
      {
        "title": "Texas Nurse Portal",
        "url": "https://txbn.boardsofnursing.org/txbn",
        "category": "portal",
        "description": "Online portal for Texas nurse services",
        "is_official": 1
      }
    ]
  }
}
400 Error: bad_request
{
  "error": "bad_request"
}

GET /v1/states/{code}/ce

Continuing education rules

The jurisdiction's CE rules, one object per rule. Two rules on the same topic stay separate; never add them together unless the record says they combine.

  • Authentication: an access token. Scope: ce:read.

It takes no query parameters: any query parameter is 400.

Authorizations

accessToken HTTP bearer: An access token from POST /v1/token (it starts npat_live_). Send it in the Authorization header only: one anywhere else is refused, and one in a URL is ended at once.

Parameters

Path parameters

NameTypeRequiredDescriptionRulesExample
codestringYesTwo capital letters: one of the codes GET /v1/states lists. Lower case or an unknown code is 404.pattern ^[A-Z]{2}$PR

Header

NameTypeRequiredDescriptionRulesExample
AuthorizationstringYesBearer, a space and your access token from POST /v1/token.-Bearer $TOKEN

Responses

200 The data, in the meta and disclaimer envelope.

Response schema: CeResponse

FieldTypeAlways present?Meaning
metaResponseMetaYes
meta.data_versionstringYesThe data release the reply comes from.
meta.generated_atstringYes, may be nullWhen that release was built, UTC (ISO 8601).
meta.tracestringYesHelps us find your requests in support. Quote it, with the X-Request-Id header.
disclaimerstringYesShow it with the data. One of: Not for clinical or licensing decisions; verify with the state board..
dataarray of CeRuleYes
data[].topicstringYesThe subject, for example General CE, Pharmacology or Implicit Bias.
data[].titlestringYesThe topic and who the rule is for, as one line.
data[].hoursnumberYes, may be nullHours the rule asks for; null when it sets none.
data[].hours_labelstringYes, may be nullThe hours in words, with the period when there is one.
data[].recurrencestringYesHow often it is owed: every_renewal, first_cycle (the first renewal after licensure), one_time, conditional (only for some licensees: see required_for), periodic (on its own schedule: see description), none (no CE needed), note (information, no hours) or expired (no longer owed). New values may be added: handle one you do not know.
data[].cycle_monthsintegerYes, may be nullThe period the hours cover, in months, when the rule names one.
data[].required_forstringYes, may be nullWho the rule applies to, in words.
data[].descriptionstringYes, may be nullThe rule in plain words.
data[].applicabilitynullYesReserved; null today.
data[].alternative_pathwaystringYes, may be nullAnother way to meet the rule, when there is one.
data[].effective_fromstringYes, may be nullThe date (YYYY-MM-DD) the rule starts, when known.
data[].effective_untilstringYes, may be nullThe date it ends, when it has an end.
data[].source_urlstringYes, may be nullThe official page for the rule.
data[].source_urlsarray of stringYesEvery official page for the rule (the first is source_url).
data[].last_verifiedstringYes, may be nullThe date the rule was last checked against its source.
data[].notesstringYes, may be nullExtra conditions, such as accreditation.
data[].rule_keystringYesA stable name for the rule: use it to track a rule between data versions.
data[].statusstringNoexpired when the rule is no longer owed; left out otherwise. One of: expired.
304 Not modified: the ETag you sent still matches. No body. It counts against the rate limit, not the monthly quota.

No body.

Errors

400 Any query parameter the route does not take, a repeated one, or array syntax.

Body: {"error":"bad_request"}

It can also answer with the errors every route shares: 304 401 403 404 405 414 429 500 503. They are on the Errors page.

Request samples

curl (Bash)
TOKEN=$(curl -s --fail -X POST https://api.npreq.com/v1/token -H "Authorization: Bearer $NPREQ_API_KEY" | jq -r .access_token)
curl -s --fail https://api.npreq.com/v1/states/PR/ce -H "Authorization: Bearer $TOKEN" \
    | jq -r '.data[] | "\(.topic): \(.hours_label) (\(.recurrence))"'
Python
import os, requests

API = "https://api.npreq.com"
t = requests.post(f"{API}/v1/token", timeout=30, headers={"Authorization": f"Bearer {os.environ['NPREQ_API_KEY']}"})
t.raise_for_status()
headers = {"Authorization": f"Bearer {t.json()['access_token']}"}

r = requests.get(f"{API}/v1/states/PR/ce", headers=headers, timeout=30)
r.raise_for_status()
for rule in r.json()["data"]:
    print(f"{rule['topic']}: {rule['hours_label']} ({rule['recurrence']})")
JavaScript
const API = "https://api.npreq.com";
const t = await fetch(`${API}/v1/token`, { method: "POST", headers: { Authorization: `Bearer ${process.env.NPREQ_API_KEY}` } });
if (!t.ok) throw new Error(`NPReq token ${t.status}`);
const headers = { Authorization: `Bearer ${(await t.json()).access_token}` };

const r = await fetch(`${API}/v1/states/PR/ce`, { headers });
if (!r.ok) throw new Error(`NPReq ${r.status}`);
for (const rule of (await r.json()).data) console.log(`${rule.topic}: ${rule.hours_label} (${rule.recurrence})`);
PowerShell
$Api = "https://api.npreq.com"
$token = (Invoke-RestMethod -Method Post -Uri "$Api/v1/token" -Headers @{ Authorization = "Bearer $env:NPREQ_API_KEY" }).access_token
$r = Invoke-RestMethod -Uri "$Api/v1/states/PR/ce" -Headers @{ Authorization = "Bearer $token" }
$r.data | ForEach-Object { "$($_.topic): $($_.hours_label) ($($_.recurrence))" }
Output
General CE: 50 hours / 36 months (every_renewal)
Dengue: 1 hours / 36 months (expired)

Response samples

200 Puerto Rico's CE rules (the whole reply)
{
  "meta": {
    "data_version": "2026.11.5",
    "generated_at": "2026-10-18T13:32:19.920846Z",
    "trace": "dc47d5852b376c6c"
  },
  "disclaimer": "Not for clinical or licensing decisions; verify with the state board.",
  "data": [
    {
      "topic": "General CE",
      "title": "General CE — Every practitioner who holds the licence",
      "hours": 50,
      "hours_label": "50 hours / 36 months",
      "recurrence": "every_renewal",
      "cycle_months": 36,
      "required_for": "Every practitioner who holds the licence",
      "description": "Advanced practice nurses recertify their license every three years on the basis of continuing education. Each recertification requires 50 hours of continuing education, and 20 of those 50 hours must be in the nurse's specialty area. The regulation's hour requirement for advanced practice nurses names only these hours and sets no separate pharmacology hours.",
      "applicability": null,
      "alternative_pathway": null,
      "effective_from": null,
      "effective_until": null,
      "source_url": "https://www.salud.pr.gov/CMS/DOWNLOAD/7291",
      "source_urls": [
        "https://www.salud.pr.gov/CMS/DOWNLOAD/7291"
      ],
      "last_verified": "2026-09-02",
      "notes": null,
      "rule_key": "PR-general_ce"
    },
    {
      "topic": "Dengue",
      "title": "Dengue — No longer owed: the triennium's dengue course deadline passed on December 31, 2024",
      "hours": 1,
      "hours_label": "1 hours / 36 months",
      "recurrence": "expired",
      "cycle_months": 36,
      "required_for": "No longer owed: the triennium's dengue course deadline passed on December 31, 2024",
      "description": "Administrative Order 2024-589-B (an amendment to Order 589, which declared a dengue public health emergency in Puerto Rico) extended until December 31, 2024 the time to complete the required education on the clinical management of dengue. That date has passed, so this one-hour course is no longer owed at renewal.",
      "applicability": null,
      "alternative_pathway": null,
      "effective_from": "2025-01-01",
      "effective_until": "2024-12-31",
      "source_url": "https://www.salud.pr.gov/CMS/261",
      "source_urls": [
        "https://www.salud.pr.gov/CMS/261"
      ],
      "last_verified": "2026-09-02",
      "notes": null,
      "rule_key": "PR-specialty_requirements",
      "status": "expired"
    }
  ]
}
400 Error: bad_request
{
  "error": "bad_request"
}

GET /health

Service health

For monitors: it needs no key or token, and it keeps answering during planned maintenance. HEAD /health answers the same with no body.

Authorizations

None: this endpoint is public, with no key or token.

Parameters

NameTypeRequiredDescriptionRulesExample
None--It takes no parameters, key or token.--

Responses

200 The API is up.

Response schema: Health

FieldTypeAlways present?Meaning
statusstringYesAlways ok. One of: ok.

Errors

403 A plain http:// request.

Body: {"error":"https_required"}

It can also answer with the errors every route shares: 405 414. They are on the Errors page.

Request samples

curl (Bash)
curl -s --fail https://api.npreq.com/health | jq -r .status
Python
import requests

r = requests.get("https://api.npreq.com/health", timeout=30)
r.raise_for_status()
print(r.json()["status"])
JavaScript
const r = await fetch("https://api.npreq.com/health");
if (!r.ok) throw new Error(`NPReq ${r.status}`);
console.log((await r.json()).status);
PowerShell
(Invoke-RestMethod -Uri "https://api.npreq.com/health").status
Output
ok

Response samples

200 Healthy
{
  "status": "ok"
}
403 Error: https_required
{
  "error": "https_required"
}

Examples

A complete client in each language. It gets a token with the key, renews it 5 minutes before it expires and once more on token_expired, then reads Texas. The key comes from the environment variable NPREQ_API_KEY. In a fleet, keep the token in a shared store and renew it from one place. The JavaScript runs server side (Node 18+ fetch, saved as a .mjs file): never ship a key or a token to a browser.

Python
import os, time, requests

API = "https://api.npreq.com"
_token = {"value": None, "expires": 0}

def token(force=False):
    # Renew 5 minutes early. In a fleet, keep this in a shared store and renew from one place.
    if force or time.time() > _token["expires"] - 300:
        r = requests.post(f"{API}/v1/token", timeout=30,
                          headers={"Authorization": f"Bearer {os.environ['NPREQ_API_KEY']}"})
        r.raise_for_status()
        t = r.json()
        _token.update(value=t["access_token"], expires=time.time() + t["expires_in"])
    return _token["value"]

def get(path):
    for attempt in (1, 2):
        r = requests.get(f"{API}{path}", timeout=30,
                         headers={"Authorization": f"Bearer {token(force=attempt == 2)}"})
        if attempt == 1 and r.status_code == 401 and r.json().get("error") == "token_expired":
            continue
        r.raise_for_status()
        return r.json()

body = get("/v1/states/TX")
print(body["meta"]["data_version"], body["data"]["name"])
JavaScript
const API = "https://api.npreq.com";
let tok = { value: null, expires: 0 };

async function token(force = false) {
  if (force || Date.now() > tok.expires - 300_000) {   // renew 5 minutes early
    const r = await fetch(`${API}/v1/token`, {
      method: "POST", headers: { Authorization: `Bearer ${process.env.NPREQ_API_KEY}` },
    });
    if (!r.ok) throw new Error(`NPReq token ${r.status}`);
    const t = await r.json();
    tok = { value: t.access_token, expires: Date.now() + t.expires_in * 1000 };
  }
  return tok.value;
}

async function get(path) {
  for (const force of [false, true]) {
    const r = await fetch(`${API}${path}`, { headers: { Authorization: `Bearer ${await token(force)}` } });
    if (!force && r.status === 401 && (await r.clone().json()).error === "token_expired") continue;
    if (!r.ok) throw new Error(`NPReq ${r.status}`);
    return r.json();
  }
}

const body = await get("/v1/states/TX");
console.log(body.meta.data_version, body.data.name);
PowerShell
$Api = "https://api.npreq.com"
$script:Token = $null
$script:RenewAt = [datetime]::MinValue

function Get-NpreqToken([switch]$Force) {
    # Renew 5 minutes early. In a fleet, keep the token in a shared store and renew from one place.
    if ($Force -or [datetime]::UtcNow -gt $script:RenewAt) {
        $t = Invoke-RestMethod -Method Post -Uri "$Api/v1/token" -Headers @{ Authorization = "Bearer $env:NPREQ_API_KEY" }
        $script:Token = $t.access_token
        $script:RenewAt = [datetime]::UtcNow.AddSeconds($t.expires_in - 300)
    }
    $script:Token
}

function Invoke-Npreq([string]$Path) {
    try {
        Invoke-RestMethod -Uri "$Api$Path" -Headers @{ Authorization = "Bearer $(Get-NpreqToken)" }
    } catch {
        if ($_.Exception.Response.StatusCode.value__ -eq 401 -and $_.ErrorDetails.Message -match 'token_expired') {
            Invoke-RestMethod -Uri "$Api$Path" -Headers @{ Authorization = "Bearer $(Get-NpreqToken -Force)" }
        } else {
            throw
        }
    }
}

$body = Invoke-Npreq "/v1/states/TX"
"$($body.meta.data_version) $($body.data.name)"
curl (Bash)
#!/usr/bin/env bash
set -euo pipefail
API=https://api.npreq.com

new_token() {   # the key goes only here
  curl -s --fail -X POST "$API/v1/token" -H "Authorization: Bearer $NPREQ_API_KEY" | jq -r .access_token
}

get() {         # one retry with a new token on token_expired
  local out code
  out=$(curl -s -w '\n%{http_code}' "$API$1" -H "Authorization: Bearer $TOKEN")
  code=${out##*$'\n'}
  if [ "$code" = 401 ] && grep -q token_expired <<<"${out%$'\n'*}"; then
    TOKEN=$(new_token)
    out=$(curl -s -w '\n%{http_code}' "$API$1" -H "Authorization: Bearer $TOKEN")
    code=${out##*$'\n'}
  fi
  [ "$code" = 200 ] || { echo "NPReq HTTP $code" >&2; return 1; }
  printf '%s\n' "${out%$'\n'*}"
}

TOKEN=$(new_token)
get /v1/states/TX | jq -r '"\(.meta.data_version) \(.data.name)"'
Output
2026.11.5 Texas

The API sends no CORS headers, so browser calls from other sites fail by design. Call it from your server.

Errors

Error bodies are short and fixed. They never contain internals. The first rows (401, 403, 429) can come from any data route; each endpoint page lists the errors that are its own.

StatusBodyMeaning
400{"error":"bad_request"}unknown, repeated or invalid query parameter; any query string on /v1/meta or /v1/token; a body over 1 KB on /v1/token
401{"error":"unauthorized"}missing, malformed, unknown, revoked or suspended key or token, or a key or token outside the Authorization header
401{"error":"use_access_token"}an API key sent to a data route: get a token from POST /v1/token
401{"error":"token_expired"}the access token is past its 6 hours: get a new one and repeat the request
403{"error":"https_required"}the request came over plain http://: use https://. A key or token in it is blocked (see Authentication)
403{"error":"forbidden"}valid key or token, but the plan lacks the scope, or the IP is not on the key's allowlist
404{"error":"not_found"}unknown path or jurisdiction code
405{"error":"method_not_allowed"}a method the route does not take: only GET on the data routes, GET or HEAD on /v1/meta and /health, POST on /v1/token; on an unknown path, any method but GET is 405 too (the Allow header names the methods)
414{"error":"uri_too_long"}path plus query string over 2 KB
429{"error":"rate_limited"}rate limit, monthly quota, more than 60 tokens in an hour for one key, too many bad keys or tokens from your IP, or, on /v1/meta, more than 30 a minute from your IP or its overall limit for all callers; wait Retry-After seconds
500{"error":"server_error"}our fault; retry later and quote X-Request-Id
503{"error":"unavailable"}planned maintenance (Retry-After: 300): every request over HTTPS except /health, which keeps answering, gets this; retry after Retry-After seconds

Repeated requests with a missing or bad key or token from one IP are refused with 429 for a while. Requests with a valid key or token are always served.

Rate limits and quotas

Responses to authenticated requests carry:

  • X-RateLimit-Limit and X-RateLimit-Remaining: requests per minute for this key (all its tokens together);
  • X-Quota-Remaining: requests left this calendar month (UTC) for your account.

The per-minute limit is per key, and an account as a whole gets at most twice it a minute across all its keys. An account has at most 2 active keys, so two keys keep their full limit each while no key you rotated away is in use. A rotated-away key still works for its 24-hour grace but adds nothing: its requests come out of the account's budget, so the other keys get less. Revoke it at once in the portal when you no longer need it. X-RateLimit-Remaining is the lower of the two. Under many parallel requests it can read a little low for a moment and then recover.

When a limit is reached you get 429 with Retry-After: seconds until the next minute for the rate limit, or until the first of next month for the quota. Retry with backoff, and never in a tight loop.

PlanScopesRequests per minute, per keyRequests per month
Startermeta, states:read, ce:read, search3010,000
Agencymeta, states:read, ce:read, search120100,000
Enterprisemeta, states:read, ce:read, search600custom

Every plan includes every endpoint, CE included; the plans differ in limits, seats and the IP allowlist.

Caching

Data responses carry an ETag and Last-Modified. Send If-None-Match with the last ETag you received. If nothing changed, you get 304 Not Modified with no body. An ETag can change even when the data has not (at least once per UTC day), so expect an occasional full 200. A 304 counts against the per-minute rate limit but not against the monthly quota. Data responses are Cache-Control: private, max-age=0 (errors are private, no-store): cache them in your own systems, not in a shared cache. /v1/meta is the exception: it is the same for everyone, so it is public, max-age=300.

Versioning

  • The path carries the API version (/v1/). Within v1, fields may be added at any time; ignore fields you do not know.
  • We give at least 90 days' notice before we remove or break a v1 field or endpoint, by email to every account owner and on this page. A breaking change ships as /v2/, and v1 keeps running until that notice period has passed.
  • If we ever discontinue the API, we give 90 days' notice and refund any unused prepaid period pro rata (see the Terms).
  • Breaking, so it ships as /v2/ with at least 90 days' notice: removing or renaming a field, an endpoint or a parameter; changing a field's type or meaning; making an optional parameter required, or adding a required one; tightening a rule so input that worked before is refused (a shorter maximum length, or a character no longer allowed); removing a value from a field that lists kinds; changing the status or the error body for the same request; changing how authentication works.
  • Not breaking, so it can happen at any time and is listed on this page: adding endpoints, fields, optional parameters or error rows; adding a new value to a field that lists kinds (recurrence, severity, practice_authority, category), so your code should handle a value it does not know; new data (a new data_version).
  • After /v2/ ships, v1 keeps answering, unchanged, at least until its 90-day notice has passed. Its retirement date is on this page and in the email.
  • Rate limits, quotas and prices are plan terms, not API versions; changes to them follow the Terms.
  • The OpenAPI specification (Download OpenAPI specification, at the top of this page) is versioned with semantic versioning within v1: a new minor version for additions, a new patch version for corrections to the documentation.
  • The API itself is the reference if this page and the API differ. Tell us at support@npreq.com and we fix the page.

Attribution

Wherever you display NPReq data, credit it as "Source: NPReq.com, data version <data_version>", with a link to npreq.com where the medium allows. Show the notice "Not for clinical or licensing decisions; verify with the state board." with it (see the Terms).

Data freshness and provenance

  • One master copy. The data comes from a single reference database that is changed only by reviewed, versioned updates ("reference packs"). npreq.com, the CeMeTracker State Data Hub and this API all serve the same build of it.
  • data_version is the newest reference pack in the build (now 2026.11.5). A new version means some state data changed; /v1/meta shows it and last_updated gives when that pack was applied.
  • Typical lag: a new version usually reaches the API within one business day of being approved. This is a typical time, not a guarantee. Every consumer's version is checked against the master daily, and a lag of more than a day is flagged to us.
  • Sources. Each fact carries the official page it came from (source), and each record lists the pages used (sources): state boards of nursing, state legislatures and administrative codes, and other official agencies. Requirement text is a plain-language reading of those sources, not a quotation of law.
  • Not advice. Boards change rules between our updates, and some rules have exceptions a summary cannot hold. Verify with the state board before acting. See the Terms.

Support

Email support@npreq.com, also for Enterprise plans. Quote the X-Request-Id header and meta.trace, and never send your key or an access token.