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
GETand returns JSON (application/json; charset=utf-8). The onePOSTis/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.
- Get a token (the key goes only here):
POST /v1/token HTTP/1.1 Host: api.npreq.com Authorization: Bearer <your API key>{"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"]} - Call the data routes with the token:
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_in21600). After that the data routes answer401 {"error":"token_expired"}: get a new token with the key and repeat the request. Renew a few minutes beforeexpires_atso 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 (
429withRetry-Afterbeyond 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/tokengets401. POST /v1/tokentakes no query string and needs no body (a body over 1 KB is400; a smaller one is ignored). Its reply isCache-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 tokensnpat_live_; they work only onapi.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 with403and{"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 usehttps://api.npreq.com. - Never put a key or a token in a URL. One sent anywhere but the
Authorizationheader (a query string, path, form body, cookie or other header) is refused with401. 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 theAuthorizationheader instead. - Optional: restrict a key to a list of IP addresses or ranges in your account. Requests from other addresses get
403, at/v1/tokenand 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.
- See the data first.
/v1/metais public: no account, key or token. It says which data release the API serves:curl -s https://api.npreq.com/v1/meta{ "data_version": "2026.11.5", "last_updated": "2026-11-02T23:52:43Z", "jurisdiction_count": 56 } - 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.
- 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:export NPREQ_API_KEY='paste your key here' - Get an access token. The key is used for one thing only: getting a token, which lasts 6 hours.
TOKEN=$(curl -s --fail -X POST https://api.npreq.com/v1/token -H "Authorization: Bearer $NPREQ_API_KEY" | jq -r .access_token) - Make your first request. Ask for the list of jurisdictions, sending the token:
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.
{
"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.
| Endpoint | Scope | Returns |
|---|---|---|
POST / | none (API key) | a 6-hour access token for the data routes |
GET / | none (public) | data version, last update, jurisdiction count |
GET / | states:read | the 56 jurisdictions: code, name, practice authority, has data |
GET / | states:read | the full record for one jurisdiction, with sources |
GET / | ce:read | continuing-education requirements for one jurisdiction |
GET / | search | up to 20 matching facts |
GET / | none | {"status": |
{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:
{
"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": {}
}| Field | Type | Always present? | Meaning |
|---|---|---|---|
meta.data_version | string | Yes | The data release the reply comes from (see Data freshness). |
meta.generated_at | string | Yes | When that release was built, UTC (ISO 8601). |
meta.trace | string | Yes | Helps us find your requests in support. Quote it, with the X-Request-Id header, when you contact us. |
disclaimer | string | Yes | "Not for clinical or licensing decisions; verify with the state board." Show it with the data. |
data | object or array | Yes | The 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
| Name | Type | Required | Description | Rules | Example |
|---|---|---|---|---|---|
Authorization | string | Yes | Bearer, 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
| Field | Type | Always present? | Meaning |
|---|---|---|---|
access_token | string | Yes | The token to send as 'Authorization: Bearer <token>' on the data routes. It starts npat_live_. |
token_type | string | Yes | Always Bearer. One of: Bearer. |
expires_in | integer | Yes | Seconds the token lasts: 21600, 6 hours. |
expires_at | string | Yes | When it stops working, UTC. |
scopes | array of string | Yes | What 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"}
Request samples
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"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"]))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(" "));$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 ' ')"Bearer token for 6 hours: ce:read meta search states:readResponse samples
{
"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"
]
}{
"error": "bad_request"
}{
"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
429withRetry-After. It counts against no monthly quota and sends noX-RateLimitorX-Quotaheaders. - It is
Cache-Control: public, max-age=300, with anETagandLast-Modified:If-None-Matchgives304.
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
| Name | Type | Required | Description | Rules | Example |
|---|---|---|---|---|---|
| None | - | - | It takes no parameters, key or token. | - | - |
Responses
200 The metadata.
Response schema: PublicMeta
| Field | Type | Always present? | Meaning |
|---|---|---|---|
data_version | string | Yes | The data release the API serves now. It changes when state data changes. |
last_updated | string | Yes, may be null | When that release's newest update was applied, UTC (ISO 8601). |
jurisdiction_count | integer | Yes | How 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"}
Request samples
curl -s --fail https://api.npreq.com/v1/meta | jq -r '"Data version \(.data_version), \(.jurisdiction_count) jurisdictions"'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")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`);$m = Invoke-RestMethod -Uri "https://api.npreq.com/v1/meta"
"Data version $($m.data_version), $($m.jurisdiction_count) jurisdictions"Data version 2026.11.5, 56 jurisdictionsResponse samples
{
"data_version": "2026.11.5",
"last_updated": "2026-11-02T23:52:43Z",
"jurisdiction_count": 56
}{
"error": "bad_request"
}{
"error": "unauthorized"
}{
"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
| Name | Type | Required | Description | Rules | Example |
|---|---|---|---|---|---|
Authorization | string | Yes | Bearer, 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
| Field | Type | Always present? | Meaning |
|---|---|---|---|
meta | ResponseMeta | Yes | |
meta. | string | Yes | The data release the reply comes from. |
meta. | string | Yes, may be null | When that release was built, UTC (ISO 8601). |
meta. | string | Yes | Helps us find your requests in support. Quote it, with the X-Request-Id header. |
disclaimer | string | Yes | Show it with the data. One of: Not for clinical or licensing decisions; verify with the state board.. |
data | array of JurisdictionSummary | Yes | |
data[]. | string | Yes | The two-letter postal code, in capitals. Use it in /v1/states/{code}. |
data[]. | string | Yes | The jurisdiction's name. |
data[]. | string | Yes, may be null | NP practice authority in a word or two, for example Full, Reduced, Restricted, Collaborative or Transitional. |
data[]. | boolean | Yes | true 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"}
Request samples
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)")'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"])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);$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)" }56 jurisdictions
AK Alaska Full
AL Alabama Collaborative
AR Arkansas TransitionalResponse samples
{
"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
}
]
}{
"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
| Name | Type | Required | Description | Rules | Example |
|---|---|---|---|---|---|
code | string | Yes | Two capital letters: one of the codes GET /v1/states lists. Lower case or an unknown code is 404. | pattern ^[A-Z]{2}$ | TX |
Header
| Name | Type | Required | Description | Rules | Example |
|---|---|---|---|---|---|
Authorization | string | Yes | Bearer, 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
| Field | Type | Always present? | Meaning |
|---|---|---|---|
meta | ResponseMeta | Yes | |
meta. | string | Yes | The data release the reply comes from. |
meta. | string | Yes, may be null | When that release was built, UTC (ISO 8601). |
meta. | string | Yes | Helps us find your requests in support. Quote it, with the X-Request-Id header. |
disclaimer | string | Yes | Show it with the data. One of: Not for clinical or licensing decisions; verify with the state board.. |
data | StateRecord | Yes | One jurisdiction's reader record. New fields may be added (see Versioning). |
data. | string | Yes | The two-letter postal code. |
data. | string | Yes | The jurisdiction's name. |
data. | string | Yes | NP practice authority in a word or two, as in /v1/states. |
data. | string | Yes | What that practice authority means there, in plain words. |
data. | array of string | Yes | The NP licence types the record covers. |
data. | number | Yes, may be null | Total CE hours per renewal cycle; null when the jurisdiction sets no single total. |
data. | integer | Yes | Length of the renewal cycle, in years. |
data. | null | Yes | Reserved; null today. |
data. | string | Yes | When the licence renews, in words. |
data. | array of any | Yes | Reserved; empty today. |
data. | string | Yes | The date (YYYY-MM-DD) the record last changed. |
data. | string | Yes | The date the record was last checked against its sources. |
data. | string | Yes | How the record was built, in words. |
data. | string | Yes | The board that licenses NPs there. |
data. | integer | Yes, may be null | 1 when NPs there have prescriptive authority; null when not recorded. |
data. | boolean | Yes | true when the record is sourced. |
data. | string | Yes | ceme for a sourced record. |
data. | string | Yes | Always sections. One of: sections. |
data. | object | Yes | Practice authority, compact membership, scope and telehealth. |
data. | string | Yes | The same as practice_authority. |
data. | array of Fact | Yes | |
data. | string | Yes | What the fact is, for example 'Renewal fee'. |
data. | string or integer | Yes | The fact, in plain words or as a number. |
data. | string | No | The 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. | array of Alert | Yes | |
data. | string | Yes | The notice. |
data. | string | Yes | The notice in full. |
data. | string | Yes | The 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. | string | No | How 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. | string | Yes, may be null | The date (YYYY-MM-DD) it takes effect, when known. |
data. | string | Yes, may be null | The official page for it. |
data. | object | Yes | Initial licensure. |
data. | string | Yes | The licence type the section describes. |
data. | array of Fact | Yes | |
data. | array of Fee | Yes | |
data. | string | Yes | What the fee is for. |
data. | number | Yes | US dollars. |
data. | string | Yes, may be null | The official page for it. |
data. | array of CeRule | Yes | The same objects as GET /v1/states/{code}/ce. |
data. | string | Yes | The subject, for example General CE, Pharmacology or Implicit Bias. |
data. | string | Yes | The topic and who the rule is for, as one line. |
data. | number | Yes, may be null | Hours the rule asks for; null when it sets none. |
data. | string | Yes, may be null | The hours in words, with the period when there is one. |
data. | string | Yes | How 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. | integer | Yes, may be null | The period the hours cover, in months, when the rule names one. |
data. | string | Yes, may be null | Who the rule applies to, in words. |
data. | string | Yes, may be null | The rule in plain words. |
data. | null | Yes | Reserved; null today. |
data. | string | Yes, may be null | Another way to meet the rule, when there is one. |
data. | string | Yes, may be null | The date (YYYY-MM-DD) the rule starts, when known. |
data. | string | Yes, may be null | The date it ends, when it has an end. |
data. | string | Yes, may be null | The official page for the rule. |
data. | array of string | Yes | Every official page for the rule (the first is source_url). |
data. | string | Yes, may be null | The date the rule was last checked against its source. |
data. | string | Yes, may be null | Extra conditions, such as accreditation. |
data. | string | Yes | A stable name for the rule: use it to track a rule between data versions. |
data. | string | No | expired when the rule is no longer owed; left out otherwise. One of: expired. |
data. | object | Yes | Renewal period, deadline, fees and process. |
data. | string | Yes | When the licence renews, in words. |
data. | array of Fact | Yes | |
data. | object | Yes | Prescriptive authority, schedules, DEA and coursework. |
data. | string | Yes | The prescribing rules in a few sentences. |
data. | array of Fact | Yes | |
data. | Board | Yes | The board that licenses NPs. Any field but name, url and portal may be null. |
data. | string | Yes, may be null | Phone. |
data. | string | Yes, may be null | Email. |
data. | string | Yes, may be null | Fax. |
data. | string | Yes, may be null | Street address. |
data. | string | Yes, may be null | City. |
data. | string | Yes, may be null | ZIP code. |
data. | string | Yes, may be null | Mailing address. |
data. | string | Yes, may be null | Physical address. |
data. | string | Yes, may be null | Office hours. |
data. | string | Yes, may be null | The board's contact page. |
data. | string | Yes, may be null | Reserved; null today. |
data. | string | Yes | The board's name. |
data. | string | Yes | The board's website. |
data. | string | Yes | The board's licensing portal. |
data. | object | Yes | The page each contact detail came from, by field name. |
data. | array of Source | Yes | The official pages the record was built from. |
data. | string | Yes | The page, in a few words. |
data. | string | Yes | The page. |
data. | array of QuickLink | Yes | Useful board pages. |
data. | string | Yes | The page title. |
data. | string | Yes | The page. |
data. | string | Yes | For example portal, renewal or fees. |
data. | string | Yes, may be null | What the page is for. |
data. | integer | Yes | 1 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"}
Request samples
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"'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")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");$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"Texas: Restricted practice authority
Renewal: Last day of birth month every 2 years
CE: 20 hours every 2 years, 13 rules
29 sourcesResponse samples
{
"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
}
]
}
}{
"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
| Name | Type | Required | Description | Rules | Example |
|---|---|---|---|---|---|
code | string | Yes | Two capital letters: one of the codes GET /v1/states lists. Lower case or an unknown code is 404. | pattern ^[A-Z]{2}$ | PR |
Header
| Name | Type | Required | Description | Rules | Example |
|---|---|---|---|---|---|
Authorization | string | Yes | Bearer, 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
| Field | Type | Always present? | Meaning |
|---|---|---|---|
meta | ResponseMeta | Yes | |
meta. | string | Yes | The data release the reply comes from. |
meta. | string | Yes, may be null | When that release was built, UTC (ISO 8601). |
meta. | string | Yes | Helps us find your requests in support. Quote it, with the X-Request-Id header. |
disclaimer | string | Yes | Show it with the data. One of: Not for clinical or licensing decisions; verify with the state board.. |
data | array of CeRule | Yes | |
data[]. | string | Yes | The subject, for example General CE, Pharmacology or Implicit Bias. |
data[]. | string | Yes | The topic and who the rule is for, as one line. |
data[]. | number | Yes, may be null | Hours the rule asks for; null when it sets none. |
data[]. | string | Yes, may be null | The hours in words, with the period when there is one. |
data[]. | string | Yes | How 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[]. | integer | Yes, may be null | The period the hours cover, in months, when the rule names one. |
data[]. | string | Yes, may be null | Who the rule applies to, in words. |
data[]. | string | Yes, may be null | The rule in plain words. |
data[]. | null | Yes | Reserved; null today. |
data[]. | string | Yes, may be null | Another way to meet the rule, when there is one. |
data[]. | string | Yes, may be null | The date (YYYY-MM-DD) the rule starts, when known. |
data[]. | string | Yes, may be null | The date it ends, when it has an end. |
data[]. | string | Yes, may be null | The official page for the rule. |
data[]. | array of string | Yes | Every official page for the rule (the first is source_url). |
data[]. | string | Yes, may be null | The date the rule was last checked against its source. |
data[]. | string | Yes, may be null | Extra conditions, such as accreditation. |
data[]. | string | Yes | A stable name for the rule: use it to track a rule between data versions. |
data[]. | string | No | expired 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"}
Request samples
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))"'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']})")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})`);$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))" }General CE: 50 hours / 36 months (every_renewal)
Dengue: 1 hours / 36 months (expired)Response samples
{
"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"
}
]
}{
"error": "bad_request"
}GET /v1/search
Search the requirements
Searches the reader text of every jurisdiction and returns up to 20 facts, ordered by jurisdiction. A fact matches when its label or value contains q, ignoring case.
- Authentication: an access token. Scope:
search.
Any other query parameter, a repeated parameter or array syntax (q[]=) 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
Query parameters
| Name | Type | Required | Description | Rules | Example |
|---|---|---|---|---|---|
q | string | Yes | 2 to 64 characters: letters, digits, spaces, hyphen, apostrophe and period. URL-encode it. | 2 to 64 characters; pattern ^[\p{L}\p{N} '.-]{2,64}$ | surcharge |
Header
| Name | Type | Required | Description | Rules | Example |
|---|---|---|---|---|---|
Authorization | string | Yes | Bearer, a space and your access token from POST /v1/token. | - | Bearer $TOKEN |
Responses
200 The data, in the meta and disclaimer envelope.
Response schema: SearchResponse
| Field | Type | Always present? | Meaning |
|---|---|---|---|
meta | ResponseMeta | Yes | |
meta. | string | Yes | The data release the reply comes from. |
meta. | string | Yes, may be null | When that release was built, UTC (ISO 8601). |
meta. | string | Yes | Helps us find your requests in support. Quote it, with the X-Request-Id header. |
disclaimer | string | Yes | Show it with the data. One of: Not for clinical or licensing decisions; verify with the state board.. |
data | array of SearchHit | Yes | |
data[]. | string | Yes | The jurisdiction the fact belongs to. |
data[]. | string | Yes | The record section it is in: overview, licensure, renewal_detail or prescribing_detail. |
data[]. | string | Yes | What the fact is. |
data[]. | string | Yes, may be null | The fact, in plain words. |
data[]. | string | Yes, may be null | The official page it came from; null when it has none of its own. |
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 q missing, too short or too long, a character not allowed, another parameter, a repeated one, or array syntax.
Body: {"error":"bad_request"}
Request samples
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 -G https://api.npreq.com/v1/search --data-urlencode "q=surcharge" -H "Authorization: Bearer $TOKEN" \
| jq -r '.data[] | "\(.code) \(.section): \(.label)"'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/search", params={"q": "surcharge"}, headers=headers, timeout=30)
r.raise_for_status()
for hit in r.json()["data"]:
print(f"{hit['code']} {hit['section']}: {hit['label']}")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/search?q=${encodeURIComponent("surcharge")}`, { headers });
if (!r.ok) throw new Error(`NPReq ${r.status}`);
for (const hit of (await r.json()).data) console.log(`${hit.code} ${hit.section}: ${hit.label}`);$Api = "https://api.npreq.com"
$token = (Invoke-RestMethod -Method Post -Uri "$Api/v1/token" -Headers @{ Authorization = "Bearer $env:NPREQ_API_KEY" }).access_token
$q = [uri]::EscapeDataString("surcharge")
$r = Invoke-RestMethod -Uri "$Api/v1/search?q=$q" -Headers @{ Authorization = "Bearer $token" }
$r.data | ForEach-Object { "$($_.code) $($_.section): $($_.label)" }PR renewal_detail: Deadline
TX overview: Scope summaryResponse samples
{
"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": "PR",
"section": "renewal_detail",
"label": "Deadline",
"value": "A nursing license is recertified every three years, counted from the registration date of each new or recertified license, and must be recertified when its registration expires, as shown on the registration certificate. Recertification may be requested 90 days before the month the license expires. A nurse who does not register and recertify the license in the required month pays the registration and recertification fees for the missed three-year period in addition to the current one, plus a surcharge for each late three-year period. When the regulation was approved, that surcharge was $50 for each late three-year period.",
"source_url": "https://www.salud.pr.gov/CMS/DOWNLOAD/5356"
},
{
"code": "TX",
"section": "overview",
"label": "Scope summary",
"value": "To prescribe or order drugs or devices, a Texas APRN needs full APRN licensure, a prescription authorization number from the Board of Nursing, and authority delegated by a physician through a prescriptive authority agreement or, in a facility-based practice, through protocols or other written authorization. The agreement must identify the types or categories of drugs or devices that may, or may not, be ordered or prescribed, and must give a general plan for consultation and referral and a plan for patient emergencies. An APRN with prescriptive authority may order or prescribe nonprescription drugs, dangerous drugs and devices, including durable medical equipment. Controlled substances in Schedules III through V may be prescribed for no more than a 90-day supply, including refills; a refill beyond the first 90 days, or a prescription for a child under two, needs a consultation with the delegating physician noted in the patient's chart. Schedule II controlled substances may be prescribed only in a hospital facility-based practice, under policies approved by the hospital's medical staff, for a patient admitted for an intended stay of 24 hours or more or receiving emergency department care, or as part of hospice care for a patient who has a written certification of terminal illness. For controlled substances the APRN must follow all federal and state laws, including the requirements of the Texas Department of Public Safety and the Drug Enforcement Administration (DEA), and each controlled substance prescription must show the DEA numbers of both the APRN and the delegating physician. Prescriptive authority is renewed together with the RN and APRN license, with an attestation of at least five contact hours of pharmacotherapeutics CE in the preceding two years, and its renewal carries a surcharge of up to $15.",
"source_url": "http://www.statutes.legis.state.tx.us/GetStatute.aspx?Code=OC&Value=157.0512"
}
]
}{
"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
| Name | Type | Required | Description | Rules | Example |
|---|---|---|---|---|---|
| None | - | - | It takes no parameters, key or token. | - | - |
Responses
200 The API is up.
Response schema: Health
| Field | Type | Always present? | Meaning |
|---|---|---|---|
status | string | Yes | Always ok. One of: ok. |
Errors
403 A plain http:// request.
Body: {"error":"https_required"}
Request samples
curl -s --fail https://api.npreq.com/health | jq -r .statusimport requests
r = requests.get("https://api.npreq.com/health", timeout=30)
r.raise_for_status()
print(r.json()["status"])const r = await fetch("https://api.npreq.com/health");
if (!r.ok) throw new Error(`NPReq ${r.status}`);
console.log((await r.json()).status);(Invoke-RestMethod -Uri "https://api.npreq.com/health").statusokResponse samples
{
"status": "ok"
}{
"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.
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"])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);$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)"#!/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)"'2026.11.5 TexasThe 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.
| Status | Body | Meaning |
|---|---|---|
| 400 | {"error": | unknown, repeated or invalid query parameter; any query string on / or /; a body over 1 KB on / |
| 401 | {"error": | missing, malformed, unknown, revoked or suspended key or token, or a key or token outside the Authorization header |
| 401 | {"error": | an API key sent to a data route: get a token from POST / |
| 401 | {"error": | the access token is past its 6 hours: get a new one and repeat the request |
| 403 | {"error": | the request came over plain http:/: use https:/. A key or token in it is blocked (see Authentication) |
| 403 | {"error": | valid key or token, but the plan lacks the scope, or the IP is not on the key's allowlist |
| 404 | {"error": | unknown path or jurisdiction code |
| 405 | {"error": | a method the route does not take: only GET on the data routes, GET or HEAD on / and /, POST on /; on an unknown path, any method but GET is 405 too (the Allow header names the methods) |
| 414 | {"error": | path plus query string over 2 KB |
| 429 | {"error": | 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 /, more than 30 a minute from your IP or its overall limit for all callers; wait Retry-After seconds |
| 500 | {"error": | our fault; retry later and quote X-Request-Id |
| 503 | {"error": | planned maintenance (Retry-After: 300): every request over HTTPS except /, 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-LimitandX-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.
| Plan | Scopes | Requests per minute, per key | Requests per month |
|---|---|---|---|
| Starter | meta, states:read, ce:read, search | 30 | 10,000 |
| Agency | meta, states:read, ce:read, search | 120 | 100,000 |
| Enterprise | meta, states:read, ce:read, search | 600 | custom |
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 newdata_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_versionis the newest reference pack in the build (now2026.11.5). A new version means some state data changed;/v1/metashows it andlast_updatedgives 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.