Extracting Refill Counts and Prescriber Details From a Prescription
10 min read · updated August 11, 2026
Most fields on a prescription can only be checked by looking at the document again. Two cannot: the prescriber’s NPI and DEA number both carry a check digit, so a transcription error in either is detectable by arithmetic on the extracted string, with no lookup and no second read. The general pattern is a checksum-validated identifier field; what follows is the two checksums these documents actually carry.
The refill field is a small vocabulary
Refills look like an integer and are not. The printed field takes a handful of forms, and a parser that expects a number drops or misreads several of them:
- A count.
0,3,11. Note that zero refills and a blank field are not the same: zero is an explicit instruction, blank is an unfilled form. - A word.
none,NRfor no refill,nil. All mean zero and all fail an integer parse. - A count with a period. “3 refills” and “refill x 3 months” are different quantities — the second is a duration from which the count has to be derived using the days supply, and deriving it is an inference you should mark as such.
- A checkbox grid. Preprinted forms often have a row of boxes labelled 0 through 5 with one ticked, which is a mark detection problem rather than a text extraction problem, and an unticked grid reads as an empty field rather than as zero.
- “prn” refills. Meaning as needed. It is not a number at all and cannot be coerced into one.
Model it as a nullable integer plus a verbatim string plus a status enumerating explicit-zero, counted, as-needed, and not-stated. The duplication is deliberate for the same reason as everywhere else in this cluster: the integer is for querying and the string is for proving what the document said.
The other prescriber-side fields — name, practice address, telephone, signature line, the date written — are ordinary text extraction with one wrinkle worth naming. The date written and the date filled are different dates and both appear; on a controlled-substance prescription the interval between them is legally significant, which means confusing the two produces an error that matters. Extract both with distinct field names, and if only one is present, leave the other null rather than copying — the ordering and plausibility checks that belong on a pair of dates like this are in date field validation.
The NPI check digit, worked
The National Provider Identifier is a ten-digit number issued through NPPES by CMS. It carries no intelligence — it does not encode specialty, state or organisation — and its tenth digit is a Luhn check digit. The wrinkle that catches implementers is that the Luhn is not computed over the nine digits alone: the constant prefix 80840 is prepended first, because the NPI is designed to work as a card issuer identifier under the ISO standard for that.
Worked on 1234567893, which is the placeholder used in coding examples and is not issued to anybody. Take the first nine digits, 123456789, prepend 80840 to get 80840123456789, then double every second digit from the right, subtracting nine from any product over nine:
payload: 8 0 8 4 0 1 2 3 4 5 6 7 8 9
\___/ = the constant 80840, prepended
Working from the right-hand end, doubling every second digit:
9 x2 = 18 -> 1+8 = 9
8 x1 = 8
7 x2 = 14 -> 5
6 x1 = 6
5 x2 = 10 -> 1
4 x1 = 4
3 x2 = 6
2 x1 = 2
1 x2 = 2
0 x1 = 0
4 x2 = 8
8 x1 = 8
0 x2 = 0
8 x1 = 8
sum = 9+8+5+6+1+4+6+2+2+0+8+8+0+8 = 67
check digit = (10 - (67 mod 10)) mod 10 = 10 - 7 = 3
The printed tenth digit is 3. It agrees.Two implementation notes. The doubling starts from the rightmost digit of the fourteen-digit payload and moves left, because the check digit is appended after it — getting the parity backwards produces a checker that rejects every valid NPI, which is at least a loud failure. And the 80840 prefix is constant and must be added before the sum, not after; omitting it produces a checker that accepts and rejects roughly at random.
The DEA check digit, worked
A DEA registration number is two letters followed by seven digits, and the seventh digit is a checksum over the first six. The first letter identifies the registrant category — practitioners and hospitals use one set, mid-level practitioners another, distributors and researchers others. The second letter is conventionally the first letter of the registrant’s last name, or a digit where the registration is under a business name.
The arithmetic: sum the first, third and fifth digits; sum the second, fourth and sixth; add twice the second sum to the first; the last digit of the total is the check digit. Worked on the synthetic number BX1234563, constructed here for the example:
DEA number: B X 1 2 3 4 5 6 3
^ ^ ^ ^ ^ ^ ^
d1..d6 check
odd positions d1 + d3 + d5 = 1 + 3 + 5 = 9
even positions d2 + d4 + d6 = 2 + 4 + 6 = 12
total = 9 + (2 x 12) = 9 + 24 = 33
last digit of 33 = 3
The printed seventh digit is 3. It agrees.
The second letter, X, would be checked against a
prescriber surname beginning with X.The surname check is a second, independent signal and it is worth running. If the extracted prescriber name and the second letter disagree, either the name or the number was misread, and the two fields are usually printed in different places on the page — so a disagreement points at a genuine transcription problem rather than at a formatting quirk.
What a checksum proves and what it does not
A passing check digit proves one thing: the digits you extracted are internally consistent, so a single-character substitution or a transposition of adjacent digits almost certainly did not happen. That is exactly the class of error OCR and manual keying produce, which is why the check is worth so much on this page. It is a validation you can run on a million records for free.
It proves nothing about existence. A number can satisfy either checksum and belong to no registrant at all — roughly one in ten random candidates will pass, since the check digit is one digit. It also proves nothing about status: a registration can be expired, surrendered or restricted, and the arithmetic is identical.
Here the two identifiers diverge sharply, and the difference should shape your pipeline. NPIs are public. CMS publishes the NPI Registry and a downloadable file of all active NPIs, so an NPI can be confirmed to exist and matched against the registrant’s name and taxonomy — which turns a format check into a genuine identity check, and lets you catch the case where a valid-looking number belongs to a different provider than the one named on the page.
DEA numbers are not public in the same way. There is no open registry to look one up in, so for most organisations the checksum and the surname letter are the whole of the offline validation available, with any further verification going through whatever authorised channel the organisation already uses. Design for that asymmetry: the NPI field can have a resolved status, and the DEA field can only have a checksum status. A schema that models them identically is promising a verification you cannot perform.
"prescriber_npi": "1234567893",
"prescriber_npi_checksum": "pass",
"prescriber_npi_registry": "matched" | "not_found" | "not_checked",
"prescriber_dea": "BX1234563",
"prescriber_dea_checksum": "pass",
"prescriber_dea_initial": "agrees_with_surname"
/* no registry field: there is no lookup */Handling: these are not the patient’s data
One distinction worth being precise about, because it changes what you may do with these fields. A prescriber’s NPI is public professional information published by CMS. A DEA registration number is not, and it is treated as sensitive precisely because it can be misused — which is why the useful design above stops at validating a number your organisation already holds and never goes near generating, guessing or completing one.
A prescription as a whole is protected health information regardless, because the prescriber fields sit on the same page as the patient fields. That has a practical consequence for how the page reaches a model: cropping to the prescriber block does not de-identify anything if the crop is generous enough to catch the patient banner, and a vision model receives the whole image regardless of which fields your schema asks for. Minimum necessary is a property of what you send, not of what you request. If a third-party model is going to see the page, the provider needs to be covered by a business associate agreement, and that includes anywhere the request is retained — logs, caches and evaluation sets are all disclosures. The general shape of that problem is covered in PII in LLM logs.