Write and sign a prescription
Your system can add patients to Priorly, write their prescriptions, and have the prescriber sign each one without leaving your system. Once signed, it goes straight to the dispensing pharmacy as an order.
The prescriber still signs every prescription themselves. Your system sends their own signing token, which says who is signing, and the PIN they type for that prescription. A token never signs on its own.
Start with Getting started: the base URL, your API key, the headers and what to do when a call fails are the same for every guide. For this one you also need:
- The signing scope on your API key
- Signing needs "Sign with a prescriber's token".
- A signing token for each prescriber
prly_rx_live_… - A business admin makes it for the prescriber in Settings › API keys, or the prescriber makes their own in Prescriptions › Sign from other systems. Store it against that prescriber. It lasts up to 12 months, and works with sandbox and live keys.
Each token is shown once, so put it straight into your secret manager.
The rules that matter most
- Call the API from your server only. Never put a key or token in a browser, an app, a URL, a log line or a prompt to an AI tool.
- Never store a prescriber's PIN. They type it in your system for each prescription. Send it in that one sign request, then discard it: no database, log, cache or queue.
- Make a new
Idempotency-Keyeach time the prescriber types their PIN. Reuse one only to retry the same request after a timeout or a5xx. - Name medicines by dm+d SNOMED code, found with
GET /medicines, never by name alone.
In the sandbox
| What | Sandbox key | Live key |
|---|---|---|
| Prescribers | The same prescribers, signing tokens and PINs | The same |
Signed with "route": "pharmacy" | A test order at Priorly's test pharmacy. Nothing is dispensed or charged | A real order at the clinic's dispensing pharmacy |
Signed with "route": "email_to_patient" | The prescription is made, and nothing is emailed | Emailed to the patient |
Signing in the sandbox is still the prescriber's. They use their real signing token and type their real PIN, and wrong PINs count towards the same 5-try lock.
This run adds a made-up patient, writes a draft, then has the prescriber sign it with a wrong PIN and then the right one. Pick your language on any code block and every block on the page follows. The IDs in the answers are examples: use the ones your calls return.
Use a sandbox key for this run. With a sandbox key, the prescription goes to Priorly's test pharmacy as a test order. With a live key, signing creates a real prescription and a real order at the pharmacy.
-
Step 1. Set up
Run these in a terminal on your own computer: Mac, Linux, or Git Bash on Windows.
read -rsasks for each secret without showing it or saving it in your shell history.Save this helper as
priorly.mjs. It needs Node 18 or later, forfetch.Save this helper as
priorly.py. It needs therequestspackage.Save this helper as
priorly.php. It needs PHP 8 with the curl extension.Add this helper to your project as
Priorly.cs. It needs .NET 8 or later, and no packages.Add this helper as
Priorly.java. It needs Java 17 or later, and Jackson'sjackson-databind.Every later step uses it. It sends your API key with each call, adds the headers each call needs, and turns an error answer into an error with its HTTP status and
code, even when the answer isn't JSON. Load the key intoPRIORLY_API_KEYfrom your secret manager when your server starts.cURLAPI=https://api.priorly.io/v1 printf 'API key: '; read -rs KEY; echo printf "Prescriber's token: "; read -rs TOKEN; echo// priorly.mjs · Node 18 or later. // Load PRIORLY_API_KEY from your secret manager. const API = 'https://api.priorly.io/v1'; export async function priorly(method, path, opts = {}) { const { body, idempotencyKey, signingToken } = opts; const key = process.env.PRIORLY_API_KEY; const headers = { Authorization: `Bearer ${key}` }; if (body) { headers['Content-Type'] = 'application/json'; } if (idempotencyKey) { headers['Idempotency-Key'] = idempotencyKey; } if (signingToken) { headers['Priorly-Prescriber-Token'] = signingToken; } const res = await fetch(API + path, { method, headers, body: body && JSON.stringify(body), // After 30 seconds, retry with the same key signal: AbortSignal.timeout(30000), }); // A proxy's 5xx may not be JSON const data = await res.json().catch(() => null); if (!res.ok) { const { status } = res; const error = data?.error ?? { code: null }; const message = error.message ?? `HTTP ${status}`; const err = new Error(message); throw Object.assign(err, error, { status }); } return data; }# priorly.py · Python 3.9 or later, with requests. # Load PRIORLY_API_KEY from your secret manager. import os import requests API = "https://api.priorly.io/v1" API_KEY = os.environ["PRIORLY_API_KEY"] session = requests.Session() session.headers["Authorization"] = f"Bearer {API_KEY}" class PriorlyError(Exception): def __init__(self, status, error): super().__init__( error.get("message") or f"HTTP {status}") self.status = status self.code = error.get("code") self.error = error def priorly(method, path, *, params=None, body=None, idempotency_key=None, signing_token=None): # requests leaves out the headers set to None headers = { "Idempotency-Key": idempotency_key, "Priorly-Prescriber-Token": signing_token, } # After 30 seconds, retry with the same key res = session.request( method, API + path, params=params, json=body, headers=headers, timeout=30) try: data = res.json() except ValueError: # a proxy's 5xx may not be JSON data = {} if not res.ok: error = data.get("error") or {} raise PriorlyError(res.status_code, error) return data<?php // priorly.php · PHP 8 or later, and its curl extension. // Load PRIORLY_API_KEY from your secret manager. const PRIORLY_API = 'https://api.priorly.io/v1'; class PriorlyError extends RuntimeException { public ?string $errorCode; public function __construct( public int $status, public array $error, ) { $this->errorCode = $error['code'] ?? null; $message = $error['message'] ?? "HTTP $status"; parent::__construct($message); } } function priorly( string $method, string $path, ?array $body = null, array $headers = [] ): array { $key = getenv('PRIORLY_API_KEY'); $headers[] = "Authorization: Bearer $key"; $ch = curl_init(PRIORLY_API . $path); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // After 30 seconds, retry with the same key curl_setopt($ch, CURLOPT_TIMEOUT, 30); if ($body !== null) { $headers[] = 'Content-Type: application/json'; $json = json_encode($body); curl_setopt($ch, CURLOPT_POSTFIELDS, $json); } curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $response = curl_exec($ch); if ($response === false) { // No answer: retry with the same key throw new RuntimeException(curl_error($ch)); } // A proxy's 5xx may not be JSON $data = json_decode($response, true); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); if ($status >= 400) { $error = $data['error'] ?? []; throw new PriorlyError($status, $error); } return $data; }// Priorly.cs · .NET 8 or later, no packages needed. // Load PRIORLY_API_KEY from your secret manager. #nullable enable using System; using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; public static class Priorly { // Paths go without a leading slash ("patients"): // one would drop the /v1. const string Api = "https://api.priorly.io/v1/"; static readonly string Key = Environment .GetEnvironmentVariable("PRIORLY_API_KEY")!; static readonly HttpClient Http = new() { BaseAddress = new Uri(Api), // After 30 seconds, retry with the same key Timeout = TimeSpan.FromSeconds(30), DefaultRequestHeaders = { Authorization = new("Bearer", Key), }, }; public static Task<JsonNode> Get(string path) => Send(HttpMethod.Get, path, null); public static Task<JsonNode> Post(string path, object body, params (string, string)[] headers) => Send(HttpMethod.Post, path, body, headers); public static Task<JsonNode> Patch(string path, object body) => Send(HttpMethod.Patch, path, body); static async Task<JsonNode> Send(HttpMethod method, string path, object? body, params (string, string)[] headers) { using var req = new HttpRequestMessage(method, path); if (body != null) req.Content = JsonContent.Create(body); foreach (var (name, value) in headers) req.Headers.Add(name, value); using var res = await Http.SendAsync(req); var raw = await res.Content.ReadAsStringAsync(); // A proxy's 5xx may not be JSON JsonNode? data = null; try { data = JsonNode.Parse(raw); } catch (JsonException) { } if (!res.IsSuccessStatusCode) throw new PriorlyException( (int)res.StatusCode, data?["error"]); return data!; } } public class PriorlyException( int status, JsonNode? error) : Exception( (string?)error?["message"] ?? $"HTTP {status}") { public int Status { get; } = status; public JsonNode? Error { get; } = error; public string? Code => (string?)Error?["code"]; }// Priorly.java · Java 17 or later, with Jackson. // Load PRIORLY_API_KEY from your secret manager. import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse.BodyHandlers; import java.time.Duration; import java.util.Map; public final class Priorly { static final String API = "https://api.priorly.io/v1"; static final String AUTH = "Bearer " + System.getenv("PRIORLY_API_KEY"); static final String JSON = "application/json"; static final HttpClient HTTP = HttpClient.newHttpClient(); static final ObjectMapper MAPPER = new ObjectMapper(); public static JsonNode get(String path) throws IOException, InterruptedException { return send("GET", path, null, Map.of()); } public static JsonNode post(String path, Object body, Map<String, String> headers) throws IOException, InterruptedException { return send("POST", path, body, headers); } public static JsonNode patch(String path, Object body) throws IOException, InterruptedException { return send("PATCH", path, body, Map.of()); } static JsonNode send(String method, String path, Object body, Map<String, String> headers) throws IOException, InterruptedException { // After 30 seconds, retry with the same key var request = HttpRequest.newBuilder() .uri(URI.create(API + path)) .timeout(Duration.ofSeconds(30)) .header("Authorization", AUTH); headers.forEach(request::header); if (body == null) { request.GET(); } else { var json = MAPPER.writeValueAsString(body); var payload = BodyPublishers.ofString(json); request.header("Content-Type", JSON) .method(method, payload); } var response = HTTP.send(request.build(), BodyHandlers.ofString()); int status = response.statusCode(); JsonNode data = parse(response.body()); if (status >= 400) { JsonNode error = data == null ? null : data.get("error"); throw new PriorlyException(status, error); } return data; } // A proxy's 5xx may not be JSON static JsonNode parse(String body) { try { return MAPPER.readTree(body); } catch (IOException e) { return null; } } public static final class PriorlyException extends RuntimeException { public final int status; public final String code; public final JsonNode error; PriorlyException(int status, JsonNode error) { super(error == null ? "HTTP " + status : error.path("message").asText()); this.status = status; this.code = error == null ? null : error.path("code").asText(null); this.error = error; } } } -
Step 2. Find who can sign at the key's location
GET/prescribers
Keep the prescriber's
id.In the answer, for each prescriber
idstring- Write a draft for this prescriber with it, as
prescriber. namestring- The prescriber's name.
regulatorstring- Who they're registered with, like
GMC, with theirregistrationNumber.
cURLcurl -s $API/prescribers -H "Authorization: Bearer $KEY"// Your own code, from here on import { randomUUID } from 'node:crypto'; import { priorly } from './priorly.mjs'; const { data: prescribers } = await priorly('GET', '/prescribers');# Your own code, from here on import uuid from priorly import PriorlyError, priorly prescribers = priorly("GET", "/prescribers")["data"]<?php // Your own code, from here on require __DIR__ . '/priorly.php'; $prescribers = priorly('GET', '/prescribers')['data'];// Your own code, from here on var prescribers = (await Priorly.Get("prescribers"))["data"];// Your own code, from here on import com.fasterxml.jackson.databind.JsonNode; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; import java.util.UUID; JsonNode prescribers = Priorly.get("/prescribers").get("data");Response200{ "data": [ { "id": "usr_9Qm2xYk7Lp", "name": "Sam Sample", "regulator": "GMC", "registrationNumber": "7654321" } ] } -
Step 3. Find the medicine's dm+d code
GET/medicines
If your system already stores dm+d codes, use those.
Query
qstringRequired- What to look for, like the medicine's name and strength: at least 3 characters.
In the answer, for each medicine
snomedCodestring- Its dm+d SNOMED code. Prescribe with this.
customMedicineIdstring- For one of the clinic's own medicines, in place of
snomedCode. packSizesarray of strings- The pack sizes it comes in.
prescribableboolean- Whether it can be prescribed.
cURLcurl -s "$API/medicines?q=finasteride%201mg" \ -H "Authorization: Bearer $KEY"const q = new URLSearchParams({ q: 'finasteride 1mg' }); const { data: medicines } = await priorly('GET', `/medicines?${q}`);found = priorly("GET", "/medicines", params={"q": "finasteride 1mg"}) medicines = found["data"]$query = http_build_query(['q' => 'finasteride 1mg']); $found = priorly('GET', "/medicines?$query"); $medicines = $found['data'];var q = Uri.EscapeDataString("finasteride 1mg"); var medicines = (await Priorly.Get($"medicines?q={q}"))["data"];String q = URLEncoder.encode("finasteride 1mg", StandardCharsets.UTF_8); JsonNode medicines = Priorly.get("/medicines?q=" + q).get("data");Response200{ "data": [ { "snomedCode": "42035911000001103", "customMedicineId": null, "name": "Finasteride 1mg tablets", "form": "Oral tablet", "packSizes": ["28", "30", "84", "90"], "controlledDrugSchedule": null, "prescribable": true } ] } -
Step 4. Add the patient
POST/patients
A home address and a date of birth are needed before a prescription can be signed. Send your own ID as
externalIdand keep Priorly'sid. Adding a patient sends them nothing.Body
externalIdstring- Your own ID for the patient. A patient whose email or
externalIdis already there is refused withpatient_exists. firstName,lastNamestringRequired- The patient's names.
dateOfBirthstringNeeded to sign- The patient's date of birth, like
1990-01-01. homeAddressobjectNeeded to sign- Where the patient lives.
line1stringRequired- The first line of the address.
line2string- The second line, if there is one.
citystring- The town or city.
postcodestringRequired- A UK postcode. It's saved in capitals with one space, so
ls14abcomes back asLS1 4AB.
cURLcurl -s $API/patients \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: test-patient-1" \ -d '{"externalId":"CLINIC-000417", "firstName":"Alex","lastName":"Example", "dateOfBirth":"1990-01-01", "homeAddress":{"line1":"1 Test Street", "city":"Leeds","postcode":"ls14ab"}}'// A new key for each patient. Reuse it only to retry. const patientKey = randomUUID(); const patient = await priorly('POST', '/patients', { idempotencyKey: patientKey, body: { externalId: 'CLINIC-000417', firstName: 'Alex', lastName: 'Example', dateOfBirth: '1990-01-01', homeAddress: { line1: '1 Test Street', city: 'Leeds', postcode: 'ls14ab', }, }, });# A new key for each patient. Reuse it only to retry. patient_key = str(uuid.uuid4()) patient = priorly( "POST", "/patients", idempotency_key=patient_key, body={ "externalId": "CLINIC-000417", "firstName": "Alex", "lastName": "Example", "dateOfBirth": "1990-01-01", "homeAddress": { "line1": "1 Test Street", "city": "Leeds", "postcode": "ls14ab", }, }, )// A new key for each patient. Reuse it only to retry. $patientKey = bin2hex(random_bytes(16)); $patient = priorly('POST', '/patients', [ 'externalId' => 'CLINIC-000417', 'firstName' => 'Alex', 'lastName' => 'Example', 'dateOfBirth' => '1990-01-01', 'homeAddress' => [ 'line1' => '1 Test Street', 'city' => 'Leeds', 'postcode' => 'ls14ab', ], ], ["Idempotency-Key: $patientKey"]);// A new key for each patient. Reuse it only to retry. var patientKey = Guid.NewGuid().ToString(); var patient = await Priorly.Post("patients", new { externalId = "CLINIC-000417", firstName = "Alex", lastName = "Example", dateOfBirth = "1990-01-01", homeAddress = new { line1 = "1 Test Street", city = "Leeds", postcode = "ls14ab", }, }, ("Idempotency-Key", patientKey));// A new key for each patient. Reuse it only to retry. String patientKey = UUID.randomUUID().toString(); JsonNode patient = Priorly.post("/patients", Map.of( "externalId", "CLINIC-000417", "firstName", "Alex", "lastName", "Example", "dateOfBirth", "1990-01-01", "homeAddress", Map.of( "line1", "1 Test Street", "city", "Leeds", "postcode", "ls14ab")), Map.of("Idempotency-Key", patientKey));Response201{ "id": "cus_8f3b2c1d9e", "externalId": "CLINIC-000417", "firstName": "Alex", "lastName": "Example", "dateOfBirth": "1990-01-01", "homeAddress": { "line1": "1 Test Street", "line2": null, "city": "Leeds", "postcode": "LS1 4AB" }, "status": "active", "…": "…" } -
Step 5. Write the draft
POST/prescriptions
Check
readyToSignistrue. The draft also appears in the prescriber's Prescriptions › Drafts in Priorly, if they'd rather sign it there.Body
patientIdstringRequired- Priorly's
idfor the patient, from step 4. prescriberstringRequired- The
idfrom step 2, or"any"for whichever prescriber signs it. externalReferencestring- Your own reference for the prescription.
itemsarray of objectsRequired- What's prescribed.
medicine.snomedCodestring- The dm+d code from step 3. Each item needs this or
customMedicineId. medicine.customMedicineIdstring- One of the clinic's own medicines from step 3, in place of
snomedCode. quantityintegerRequired- How many, as a whole number.
unitstring- What the quantity counts, like
tablets. directionsstringRequired- The directions for the patient.
In the answer
readyToSignbooleantruewhen it can be signed.problemsarray of objects- What's stopping it, each with a
codeand amessage: usually the patient's home address or date of birth.
cURLcurl -s $API/prescriptions \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: test-rx-1" \ -d '{"patientId":"cus_8f3b2c1d9e", "prescriber":"any", "externalReference":"RX-TEST-1", "items":[{ "medicine":{"snomedCode":"42035911000001103"}, "quantity":28,"unit":"tablets", "directions":"Take ONE tablet daily."}]}'// A new key for each draft. Reuse it only to retry. const draftKey = randomUUID(); const draft = await priorly('POST', '/prescriptions', { idempotencyKey: draftKey, body: { patientId: patient.id, prescriber: 'any', externalReference: 'RX-TEST-1', items: [{ medicine: { snomedCode: '42035911000001103' }, quantity: 28, unit: 'tablets', directions: 'Take ONE tablet daily.', }], }, });item = { "medicine": {"snomedCode": "42035911000001103"}, "quantity": 28, "unit": "tablets", "directions": "Take ONE tablet daily.", } # A new key for each draft. Reuse it only to retry. draft_key = str(uuid.uuid4()) draft = priorly( "POST", "/prescriptions", idempotency_key=draft_key, body={ "patientId": patient["id"], "prescriber": "any", "externalReference": "RX-TEST-1", "items": [item], }, )$medicine = ['snomedCode' => '42035911000001103']; $item = [ 'medicine' => $medicine, 'quantity' => 28, 'unit' => 'tablets', 'directions' => 'Take ONE tablet daily.', ]; // A new key for each draft. Reuse it only to retry. $draftKey = bin2hex(random_bytes(16)); $draft = priorly('POST', '/prescriptions', [ 'patientId' => $patient['id'], 'prescriber' => 'any', 'externalReference' => 'RX-TEST-1', 'items' => [$item], ], ["Idempotency-Key: $draftKey"]);var item = new { medicine = new { snomedCode = "42035911000001103" }, quantity = 28, unit = "tablets", directions = "Take ONE tablet daily.", }; // A new key for each draft. Reuse it only to retry. var draftKey = Guid.NewGuid().ToString(); var draft = await Priorly.Post("prescriptions", new { patientId = (string?)patient["id"], prescriber = "any", externalReference = "RX-TEST-1", items = new[] { item }, }, ("Idempotency-Key", draftKey));var medicine = Map.of("snomedCode", "42035911000001103"); Map<String, Object> item = Map.of( "medicine", medicine, "quantity", 28, "unit", "tablets", "directions", "Take ONE tablet daily."); // A new key for each draft. Reuse it only to retry. String draftKey = UUID.randomUUID().toString(); JsonNode draft = Priorly.post("/prescriptions", Map.of( "patientId", patient.get("id").asText(), "prescriber", "any", "externalReference", "RX-TEST-1", "items", List.of(item)), Map.of("Idempotency-Key", draftKey));Response201{ "id": "script_AB12-CD34-EF56-GH78", "status": "draft", "readyToSign": true, "problems": [], "prescriber": null, "…": "…" } -
Step 6. The prescriber signs
POST/prescriptions/{id}/sign
In your system they check the prescription and type their PIN; your server sends it with their token. Here, the prescriber types it at the prompt, and
printfhands it to curl through a pipe, so it never shows in the list of running programs.Here,pinis what they just typed,signingTokenis their token from your secret manager, andpinAttemptKeyis new for each PIN they type.Here,pinis what they just typed,signing_tokenis their token from your secret manager, andpin_attempt_keyis new for each PIN they type.Here,$pinis what they just typed,$signingTokenis their token from your secret manager, and$pinAttemptKeyis new for each PIN they type.Headers
Priorly-Prescriber-TokenstringRequired- The signing token of the prescriber who's signing.
Body
pinstringRequired- The 4-digit PIN the prescriber just typed, as a string. Send it in this one request, then discard it.
routestringpharmacy, the default, sends it to the clinic's dispensing pharmacy as an order.email_to_patientemails it to the patient.
A wrong PIN is refused, and nothing is signed. Run it again with a new
Idempotency-Key(test-sign-2) and the right PIN, and it's signed and sent.When they type the right PIN, your code sends it with a new key, and it's signed and sent.With a sandbox key,
pharmacyis Priorly's test pharmacy and the order is a test order. With a live key, it's the clinic's own dispensing pharmacy.If that request times out, send it again with the same
Idempotency-Key. You get the first answer back withIdempotent-Replayed: true, and it is never signed twice.cURLRX=script_AB12-CD34-EF56-GH78 printf 'Prescriber, your PIN: '; read -rs PIN; echo printf '{"pin":"%s","route":"pharmacy"}' "$PIN" | curl -s $API/prescriptions/$RX/sign \ -H "Authorization: Bearer $KEY" \ -H "Priorly-Prescriber-Token: $TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: test-sign-1" \ --data-binary @- unset PIN// pin is what the prescriber just typed. signingToken // is their token, from your secret manager. Send the // PIN in this one request, then let it go: never log, // store or queue it. Make a new key for each PIN typed. const pinAttemptKey = randomUUID(); const signPath = `/prescriptions/${draft.id}/sign`; const signed = await priorly('POST', signPath, { idempotencyKey: pinAttemptKey, signingToken, body: { pin, route: 'pharmacy' }, });# pin is what the prescriber just typed. signing_token # is their token, from your secret manager. Send the # PIN in this one request, then let it go: never log, # store or queue it. Make a new key for each PIN typed. pin_attempt_key = str(uuid.uuid4()) sign_path = f"/prescriptions/{draft['id']}/sign" signed = priorly( "POST", sign_path, idempotency_key=pin_attempt_key, signing_token=signing_token, body={"pin": pin, "route": "pharmacy"}, )// $pin is what the prescriber just typed. $signingToken // is their token, from your secret manager. Send the // PIN in this one request, then let it go: never log, // store or queue it. Make a new key for each PIN typed. $pinAttemptKey = bin2hex(random_bytes(16)); $signPath = "/prescriptions/{$draft['id']}/sign"; $signed = priorly('POST', $signPath, [ 'pin' => $pin, 'route' => 'pharmacy', ], [ "Idempotency-Key: $pinAttemptKey", "Priorly-Prescriber-Token: $signingToken", ]);// pin is what the prescriber just typed. signingToken // is their token, from your secret manager. Send the // PIN in this one request, then let it go: never log, // store or queue it. Make a new key for each PIN typed. var pinAttemptKey = Guid.NewGuid().ToString(); var draftId = (string?)draft["id"]; var signPath = $"prescriptions/{draftId}/sign"; var signed = await Priorly.Post(signPath, new { pin, route = "pharmacy" }, ("Idempotency-Key", pinAttemptKey), ("Priorly-Prescriber-Token", signingToken));// pin is what the prescriber just typed. signingToken // is their token, from your secret manager. Send the // PIN in this one request, then let it go: never log, // store or queue it. Make a new key for each PIN typed. String pinAttemptKey = UUID.randomUUID().toString(); String draftId = draft.get("id").asText(); String signPath = "/prescriptions/" + draftId + "/sign"; Map<String, String> signHeaders = Map.of( "Idempotency-Key", pinAttemptKey, "Priorly-Prescriber-Token", signingToken); JsonNode signed = Priorly.post(signPath, Map.of("pin", pin, "route", "pharmacy"), signHeaders);Response · a wrong PIN422{ "error": { "code": "wrong_pin", "message": "That PIN isn’t right. Ask the prescriber to type it again.", "attemptsLeft": 4, "requestId": "req_GI4BAJMiqdr3tA4s" } }Response · the right PIN200{ "id": "script_AB12-CD34-EF56-GH78", "status": "sent_to_pharmacy", "route": "pharmacy", "signedThrough": "api", "signedBy": { "id": "usr_9Qm2xYk7Lp", "name": "Sam Sample", "regulator": "GMC", "registrationNumber": "7654321" }, "pharmacy": { "name": "Parkside Pharmacy" }, "orderNumber": "40123", "…": "…" } -
Step 7. Follow it
GET/prescriptions/{id}
It shows the status as it moves to
dispensed, withtrackingonce it's posted. A prescription's status is one of these:draftWritten, and waiting to be signed.
sent_to_pharmacySigned and sent to the pharmacy as an order.
not_sentSigned, but it couldn't reach the pharmacy yet. The prescriber can send it again in Priorly.
partly_dispensedSome of its items dispensed.
dispensedDispensed at the pharmacy.
not_dispensedThe pharmacy cancelled the order.
emailedSigned and emailed to the patient, to take to any pharmacy.
voidedCancelled by a prescriber after it was signed.
draft_deletedDeleted before it was signed.
for_orderMade in Priorly for an approved online order. Your system doesn't write these.
Or add a webhook in Settings › API keys, and Priorly tells you when it's signed, sent, dispensed and posted.
cURLcurl -s $API/prescriptions/$RX \ -H "Authorization: Bearer $KEY"const rx = await priorly('GET', `/prescriptions/${draft.id}`);rx = priorly("GET", f"/prescriptions/{draft['id']}")$rx = priorly('GET', "/prescriptions/{$draft['id']}");var rx = await Priorly.Get($"prescriptions/{draftId}");JsonNode rx = Priorly.get("/prescriptions/" + draftId);Response · once posted200{ "id": "script_AB12-CD34-EF56-GH78", "status": "dispensed", "tracking": [ { "carrier": "Royal Mail", "number": "AB123456789GB", "url": null } ], "…": "…" }
Every error has the same shape, {"error":{"code","message",…,"requestId"}}. What your system should do with the ones signing meets most:
status: sent_to_pharmacy200Signed and sent.
Show it as sent, with orderNumber.
status: not_sent200Signed, but it couldn't reach the pharmacy yet.
Tell the prescriber: they can send it again in Priorly.
wrong_pin422The PIN is wrong. attemptsLeft says how many tries are left.
Ask the prescriber to type it again, and send it with a new Idempotency-Key. Never resend a PIN yourself.
pin_locked4235 wrong PINs in a row. Signing is locked until lockedUntil (15 minutes), in Priorly too.
Tell the prescriber, and don't retry.
not_ready_to_sign422The draft has problems, usually the patient's home address or date of birth.
PATCH /patients/{id} with what's missing, then ask for the PIN again.
not_your_draft403The draft was written for another prescriber.
Have that prescriber sign it, or write drafts for "any".
invalid_prescriber_token401The token was revoked or expired, the prescriber changed or reset their password, or they no longer prescribe at the clinic.
Ask for a new token for that prescriber.
not_a_draft409Already signed, voided or deleted.
GET the prescription for its status.
patient_exists409Adding a patient whose email or externalId is already there.
PATCH the existingPatientId it gives instead.
Too many requests, or a fault at Priorly.
Retry with backoff and the same Idempotency-Key. A 429 says how long to wait in Retry-After.
Any other 4xx means the request needs fixing, so don't repeat it unchanged.
{
"error": {
"code": "wrong_pin",
"message": "That PIN isn’t right. Ask the prescriber to type it again.",
"attemptsLeft": 4,
"requestId": "req_GI4BAJMiqdr3tA4s"
}
}
printf 'Prescriber, your PIN: '; read -rs PIN; echo
answer=$(
printf '{"pin":"%s","route":"pharmacy"}' "$PIN" |
curl -s $API/prescriptions/$RX/sign \
-H "Authorization: Bearer $KEY" \
-H "Priorly-Prescriber-Token: $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: test-sign-3" \
--data-binary @-)
unset PIN
code=$(jq -r '.error.code // empty' <<< "$answer")
case "$code" in
"")
jq -r .status <<< "$answer" ;;
wrong_pin)
echo "Wrong PIN. Ask them to type it again." ;;
pin_locked)
echo "Signing is locked. Don't retry." ;;
not_ready_to_sign)
echo "Fill in the patient's details, then sign." ;;
invalid_prescriber_token)
echo "Ask for a new signing token." ;;
*)
echo "$code" ;;
esac
// pinAttemptKey: made when they typed this PIN
try {
const answer = await priorly('POST', signPath, {
idempotencyKey: pinAttemptKey,
signingToken,
body: { pin, route: 'pharmacy' },
});
// answer.status is sent_to_pharmacy or not_sent
} catch (err) {
// Branch on err.code, never on err.message
switch (err.code) {
case 'wrong_pin':
// Ask them to type it again. Tries left:
// err.attemptsLeft
break;
case 'pin_locked':
// Don't retry. Locked until err.lockedUntil
break;
case 'not_ready_to_sign':
// GET the draft for its problems, then PATCH
// the patient with what's missing
break;
case 'invalid_prescriber_token':
// Ask for a new signing token
break;
default:
throw err;
}
}
# pin_attempt_key: made when they typed this PIN
try:
answer = priorly(
"POST", sign_path,
idempotency_key=pin_attempt_key,
signing_token=signing_token,
body={"pin": pin, "route": "pharmacy"},
)
# answer["status"] is sent_to_pharmacy or not_sent
except PriorlyError as e:
# Branch on e.code, never on the message
if e.code == "wrong_pin":
# Ask them to type it again. Tries left:
# e.error["attemptsLeft"]
pass
elif e.code == "pin_locked":
# Don't retry. Locked until
# e.error["lockedUntil"]
pass
elif e.code == "not_ready_to_sign":
# GET the draft for its problems, then PATCH
# the patient with what's missing
pass
elif e.code == "invalid_prescriber_token":
# Ask for a new signing token
pass
else:
raise
// $pinAttemptKey: made when they typed this PIN
try {
$answer = priorly('POST', $signPath, [
'pin' => $pin,
'route' => 'pharmacy',
], [
"Idempotency-Key: $pinAttemptKey",
"Priorly-Prescriber-Token: $signingToken",
]);
// $answer['status'] is sent_to_pharmacy or not_sent
} catch (PriorlyError $e) {
// Branch on errorCode, never on the message
$code = $e->errorCode;
if ($code === 'wrong_pin') {
// Ask them to type it again. Tries left:
// $e->error['attemptsLeft']
} elseif ($code === 'pin_locked') {
// Don't retry. Locked until
// $e->error['lockedUntil']
} elseif ($code === 'not_ready_to_sign') {
// GET the draft for its problems, then PATCH
// the patient with what's missing
} elseif ($code === 'invalid_prescriber_token') {
// Ask for a new signing token
} else {
throw $e;
}
}
// pinAttemptKey: made when they typed this PIN
try
{
var answer = await Priorly.Post(signPath,
new { pin, route = "pharmacy" },
("Idempotency-Key", pinAttemptKey),
("Priorly-Prescriber-Token", signingToken));
// answer["status"] is sent_to_pharmacy or not_sent
}
// Branch on Code, never on the message
catch (PriorlyException e)
when (e.Code == "wrong_pin")
{
// Ask them to type it again. Tries left:
// e.Error?["attemptsLeft"]
}
catch (PriorlyException e)
when (e.Code == "pin_locked")
{
// Don't retry. Locked until e.Error?["lockedUntil"]
}
catch (PriorlyException e)
when (e.Code == "not_ready_to_sign")
{
// GET the draft for its problems, then PATCH
// the patient with what's missing
}
catch (PriorlyException e)
when (e.Code == "invalid_prescriber_token")
{
// Ask for a new signing token
}
// signHeaders: from step 6, with this PIN's key
try {
JsonNode answer = Priorly.post(signPath,
Map.of("pin", pin, "route", "pharmacy"),
signHeaders);
// answer's "status": sent_to_pharmacy or not_sent
} catch (Priorly.PriorlyException e) {
// Branch on e.code, never on the message
switch (String.valueOf(e.code)) {
case "wrong_pin" -> {
// Ask them to type it again. Tries left:
// e.error.get("attemptsLeft")
}
case "pin_locked" -> {
// Don't retry. Locked until
// e.error.get("lockedUntil")
}
case "not_ready_to_sign" -> {
// GET the draft for its problems, then
// PATCH the patient with what's missing
}
case "invalid_prescriber_token" -> {
// Ask for a new signing token
}
default -> throw e;
}
}
Check that your system:
0 of 6 checked
When something doesn't work, send Priorly the requestId from the error, or the Priorly-Request-Id header on any answer. Never send a key, token, PIN or patient details. Priorly can send the full API reference, which covers every field, error and webhook.