5.4.1 DocumentSearchRequest Element Require
Onboarding Step by Step Process
These steps outline the process to onboard a customer to Surescripts Data Solutions, from account setup through data downloads.
ăndarăt
Note: This is Early Adopter documentation. Requirements and content are subject to change.

Step 1: Verify Access to Data Package Builder in Workbench
- For instructions on how to set up access, see the Setting Up Access to Workbench & Data Package BuilderSetting Up Access to Workbench & Data Package Builder instructions.
Step 2: Generate Keys
Step 2a: Generate RSA Key Pairs for Data Package Builder Authentication
To authenticate with the Data Package Builder API, you need to generate a 2048-bit RSA key pair and upload the public key through the Client Keys interface.
Option 1: Using Open SSL (Recommended - Works on Linux, macOS, Windows)
- Generate a 2048-bit RSA private key.

openssl genrsa -out private_key.pem 2048- Extract the public key.
openssl req -new -x509 -key privatekey.pem -out publickey509.pem -subj '/CN=data-package-builder'- Verify your keys pairs.
# View the private key
cat privatekey.pem
# View the public key
cat publickey509.pemOption 2: Using PowerShell (Windows)
- Generate RSA key pair.
$rsa = [System.Security.Cryptography.RSA]::Create(2048)- Export public key in X.509 SubjectPublicKeyInfo format.
$publicKey = $rsa.ExportSubjectPublicKeyInfo()
$publicPem = "-----BEGIN PUBLIC KEY-----`n" +
[Convert]::ToBase64String($publicKey, 'InsertLineBreaks') +
"`n-----END PUBLIC KEY-----"
$publicPem | Out-File -FilePath "public_key.pem" -Encoding ASCII- Export private key.
$privateKey = $rsa.ExportRSAPrivateKey()
$privatePem = "-----BEGIN RSA PRIVATE KEY-----`n" +
[Convert]::ToBase64String($privateKey, 'InsertLineBreaks') +
"`n-----END RSA PRIVATE KEY-----"
$privatePem | Out-File -FilePath "private_key.pem" -Encoding ASCII
Write-Host "Keys generated successfully!"
Write-Host "Public key is in X.509 SubjectPublicKeyInfo format"What You'll Get:

After generation of the key pair, you will have two files:
- private_key.pem - Your private key - CRITICAL: Keep this secure - Store it in a secure location (HSM, key vault, encrypted storage) - This will be used to sign a compact JWT to request an acess token from the authorization server
- public_key.pem - Your public key - This is what you upload to Data Package Builder - Safe to share (it's called "public" for a reason) - Example format: -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Z3VS5JJcds3... -----END PUBLIC KEY-----
Step 2b: Upload your Public Key
- Log in to Data Package Builder.
- Navigate to Client Keys.
- Paste the entire contents of public_key.pem (including the BEGIN/END lines).
- Click Create Client.
- Note: This will create the Client_ID that the customer will later use in Build the Compact JWT AssertionBuild the Compact JWT Assertion section.
⚠️ Important: Once you have created your Client ID, notify your Surescripts point of contact. Surescripts must configure your data package before you can proceed to the next steps.
You will not be able to continue until this configuration is complete.

Security Best Practices:
✅ DO:
- Generate keys in a secure environment
- Store private keys in a key vault or HSM for production use
- Use strong file permissions on private key files (chmod 600 private_key.pem on Unix)
- Rotate keys periodically
❌ DON'T:
- Share or commit private keys to version control
- Email private keys
- Store private keys in plaintext in application code
- Reuse the same key pair across multiple clients/applications
Step 2c: Common errors
The below table includes information on common errors that can be encountered when creating key pairs.
Error | Troubleshooting Information |
|---|---|
"Invalid certificate/public key format" error |
|
Key appears to upload but authentication fails |
|
Step 3: Query Builder
The Query Builder in Workbench helps you create an OData query URL that defines exactly which prescribed medication data you want to retrieve from the Data Solutions API. You can build a query visually or edit it directly, depending on your preference.
Note: The Query Builder is optional; experienced users may submit manual OData queries directly.
OData is not the only type of request that is supported. For additional information on other uses for the Query Builder, see Query Builder OverviewQuery Builder Overview .
For additional information on OData, see https://learn.microsoft.com/en-us/odata/overview
Important: Before you begin:
- You must be able to sign in to Workbench.
- Your user must have access to Data Package Builder with the Query Edit role.
- You must be working with the Prescribed Medication product in Data Solutions.
- For instructions on this, see Setting Up Access to Workbench & Data Package BuilderSetting Up Access to Workbench & Data Package Builder.
Step 3a: Run the Query Builder (OData)
- Sign in to Workbench and open Data Package Builder.
- Select Query Builder.
- Choose the Prescribed Medication product.
- Select the columns you want returned (these map to $select in OData).
- Add filters to limit results.
- A PARTITION_DAY filter is required.
- Select Generate from Selections to automatically build the OData query (or manually edit the query if you’re familiar with OData).
- Copy the generated OData query URL.
- Use the URL in your Data Solutions API GET request to retrieve Prescribed Medication data.
Note: For information on how to run the Query Builder for other use cases, see Key Steps (Applies to All Use Cases)Key Steps (Applies to All Use Cases)
Step 4: Determining PARTITION_DAY Filter
To determine which PARTITION_DAY filter to request, see the How to Use the EndpointsHow to Use the Endpoints page.
Step 5: Send REST Requests to Data Solutions API
Authentication
Learn how to authenticate with the Data Solutions API using OAuth 2.0 Client Credentials Grant with JWT Bearer Assertion.
What You'll Do
The Data Solutions API uses a two-step authentication process:
- Build a JWT assertion - Create and sign a JWT claim using your ClientID and private key
- Request an access token - Exchange the JWT assertion for an OAuth access token at the auth-server's /connect/token endpoint
- Use the access token - Include the access token in API requests
┌─────────────┐ ┌─────────────┐
│ Client │ │ Auth Server │
└──────┬──────┘ └──────┬──────┘
│ │
│ 1. Build JWT assertion │
│ (signed with private key) │
│ │
│ 2. POST /connect/token │
│ (client_assertion = JWT) │
├─────────────────────────────────────────────>│
│ │
│ 3. Access token response │
│<─────────────────────────────────────────────┤
│ │
│ │
│ ┌──────────────────┴─────┐
│ │ Data Solutions API │
│ └──────────────────┬─────┘
│ │
│ 4. API request with access token │
├─────────────────────────────────────────────>│
│ │
│ 5. Data response │
│<─────────────────────────────────────────────┤
│ │Before You Begin
Make sure you have:
- Client ID - A unique identifier for your application.
- Private Key - An RSA private key in PKCS#8 PEM format.
- Public Key Registration - Your public key must be registered with the auth server for your Client ID.
Private Key Format
Your private key must be in PKCS#8 PEM format:
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...
... base64 encoded key data ...
-----END PRIVATE KEY-----Step 5a: Build the Compact JWT Assertion
Create a JWT with the following structure, signed with your private key using the RS384 algorithm (RSASSA-PKCS1-v1_5 with SHA-384). This JWT proves your identity to the auth server.
JWT Header
{
"alg": "RS384",
"typ": "JWT"
}JWT Payload
{
"iss": "your-client-id", // Issuer: your client ID
"sub": "your-client-id", // Subject: your client ID
"aud": "ENVIRONMENT_AUTH_URL/connect/token", // Audience: token endpoint URL
"jti": "550e8400-e29b-41d4-a716-446655440000", // JWT ID: unique identifier (UUID)
"iat": 1706572800, // Issued at: current Unix timestamp
"nbf": 1706572800, // Not before: current Unix timestamp
"exp": 1706573100 // Expires: current timestamp + 5 minutes
}Important field notes:
- iss and sub must both be your Client ID
- aud must be the exact token endpoint URL (including /connect/token)
- jti must be unique for each request (use a UUID)
- exp should be 5 minutes from iat (recommended maximum)
Example: Building JWT in Python
import jwt
import uuid
import time
from datetime import datetime, timedelta
# Your credentials
client_id = "your-client-id"
private_key = """-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...
-----END PRIVATE KEY-----"""
token_endpoint = "ENVIRONMENT_AUTH_URL/connect/token"
# Build the JWT payload
now = int(time.time())
payload = {
"iss": client_id,
"sub": client_id,
"aud": token_endpoint,
"jti": str(uuid.uuid4()),
"iat": now,
"nbf": now,
"exp": now + 300 # 5 minutes from now
}
# Sign the JWT with RS384
client_assertion = jwt.encode(
payload,
private_key,
algorithm="RS384"
)
print(f"JWT Assertion: {client_assertion}")Example: Building JWT in C#
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
// Your credentials
var clientId = "your-client-id";
var privateKeyPem = @"-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...
-----END PRIVATE KEY-----";
var tokenEndpoint = "ENVIRONMENT_AUTH_URL/connect/token";
// Load the private key
using var rsa = RSA.Create();
rsa.ImportFromPem(privateKeyPem);
var signingCredentials = new SigningCredentials(
new RsaSecurityKey(rsa),
SecurityAlgorithms.RsaSha384
);
// Build the JWT
var now = DateTime.UtcNow;
var handler = new JwtSecurityTokenHandler();
var token = handler.CreateJwtSecurityToken(
issuer: clientId,
audience: tokenEndpoint,
subject: new ClaimsIdentity(new[] { new Claim("sub", clientId) }),
notBefore: now,
expires: now.AddMinutes(5),
issuedAt: now,
signingCredentials: signingCredentials
);
// Add the jti claim
token.Payload["jti"] = Guid.NewGuid().ToString();
var clientAssertion = handler.WriteToken(token);
Console.WriteLine($"JWT Assertion: {clientAssertion}");Example: Building JWT in JavaScript/Node.js
const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid');
const fs = require('fs');
// Your credentials
const clientId = 'your-client-id';
const privateKey = fs.readFileSync('private_key.pem', 'utf8');
const tokenEndpoint = 'ENVIRONMENT_AUTH_URL/connect/token';
// Build the JWT payload
const now = Math.floor(Date.now() / 1000);
const payload = {
iss: clientId,
sub: clientId,
aud: tokenEndpoint,
jti: uuidv4(),
iat: now,
nbf: now,
exp: now + 300 // 5 minutes
};
// Sign the JWT with RS384
const clientAssertion = jwt.sign(payload, privateKey, {
algorithm: 'RS384'
});
console.log(`JWT Assertion: ${clientAssertion}`);Step 5b: Request Access Token
Send a POST request to the auth server's /connect/token endpoint with the JWT assertion.
Request Format
POST /connect/token HTTP/1.1
Host: ENVIRONMENT_AUTH_URL
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=your-client-id
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=<your-signed-jwt>
&scope=DsaApiParameters:
- grant_type - Must be client_credentials
- client_id - Your client identifier
- client_assertion_type - Must be urn:ietf:params:oauth:client-assertion-type:jwt-bearer
- client_assertion - The signed JWT from Step 1
- scope - Must be DsaApi to access the Data Solutions API
Example: Request Token in Python
import requests
# Build the token request
token_url = "ENVIRONMENT_AUTH_URL/connect/token"
data = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": client_assertion,
"scope": "DsaApi"
}
# Request the access token
response = requests.post(token_url, data=data)
response.raise_for_status()
token_response = response.json()
access_token = token_response["access_token"]
expires_in = token_response["expires_in"]
print(f"Access Token: {access_token}")
print(f"Expires in: {expires_in} seconds")Example: Request Token in C#
using System.Net.Http;
var httpClient = new HttpClient();
var tokenUrl = "ENVIRONMENT_AUTH_URL/connect/token";
var requestData = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", clientId },
{ "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" },
{ "client_assertion", clientAssertion },
{ "scope", "DsaApi" }
};
var response = await httpClient.PostAsync(tokenUrl, new FormUrlEncodedContent(requestData));
response.EnsureSuccessStatusCode();
var tokenResponse = await response.Content.ReadFromJsonAsync<TokenResponse>();
var accessToken = tokenResponse.AccessToken;
var expiresIn = tokenResponse.ExpiresIn;
Console.WriteLine($"Access Token: {accessToken}");
Console.WriteLine($"Expires in: {expiresIn} seconds");
// Token response model
public class TokenResponse
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
[JsonPropertyName("token_type")]
public string TokenType { get; set; }
}Example: Request Token in JavaScript/Node.js
const axios = require('axios');
const qs = require('querystring');
const tokenUrl = 'ENVIRONMENT_AUTH_URL/connect/token';
const requestData = {
grant_type: 'client_credentials',
client_id: clientId,
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
client_assertion: clientAssertion,
scope: 'DsaApi'
};
axios.post(tokenUrl, qs.stringify(requestData), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
})
.then(response => {
const accessToken = response.data.access_token;
const expiresIn = response.data.expires_in;
console.log(`Access Token: ${accessToken}`);
console.log(`Expires in: ${expiresIn} seconds`);
})
.catch(error => {
console.error('Token request failed:', error.response?.data || error.message);
});Successful Response
{
"access_token": "eyJhbGciOiJSUzM4NCIsImtpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJKV1QifQ...",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "DsaApi"
}Error Response
{
"error": "invalid_client",
"error_description": "Invalid client or client credentials"
}Common error codes:
- invalid_client - Client ID not found or public key mismatch
- invalid_grant - JWT assertion validation failed
- invalid_scope - Requested scope not available
Step 5c: Use Access Token in Data Solutions API Requests
Include the access token in the Authorization header of your API requests.
Request Format
GET /api/v0/odata/PrescribedMedicationData?$filter=PARTITION_DAY eq '2025-01-30' HTTP/1.1
Host: ENVIRONMENT_DATA_SOLUTIONS_URL
Authorization: Bearer <your-access-token>Example: API Request in Python
import requests
api_url = "ENVIRONMENT_DATA_SOLUTIONS_URL/api/v0/odata/PrescribedMedicationData"
headers = {
"Authorization": f"Bearer {access_token}"
}
params = {
"$filter": "PARTITION_DAY eq '2025-01-30'",
"$top": 10
}
response = requests.get(api_url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
print(f"Retrieved {len(data['value'])} records")Example: API Request in C#
var apiUrl = "ENVIRONMENT_DATA_SOLUTIONS_URL/api/v0/odata/PrescribedMedicationData";
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
var queryParams = new Dictionary<string, string>
{
{ "$filter", "PARTITION_DAY eq '2025-01-30'" },
{ "$top", "10" }
};
var queryString = string.Join("&", queryParams.Select(kvp =>
$"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value)}"));
var requestUrl = $"{apiUrl}?{queryString}";
var response = await httpClient.GetAsync(requestUrl);
response.EnsureSuccessStatusCode();
var data = await response.Content.ReadFromJsonAsync<ODataResponse>();
Console.WriteLine($"Retrieved {data.Value.Count} records");Example: API Request in JavaScript/Node.js
const axios = require('axios');
const apiUrl = 'ENVIRONMENT_DATA_SOLUTIONS_URL/api/v0/odata/PrescribedMedicationData';
const headers = {
'Authorization': `Bearer ${accessToken}`
};
const params = {
'$filter': "PARTITION_DAY eq '2025-01-30'",
'$top': 10
};
axios.get(apiUrl, { headers, params })
.then(response => {
console.log(`Retrieved ${response.data.value.length} records`);
})
.catch(error => {
console.error('API request failed:', error.response?.data || error.message);
});Step 5d: Handle Token Expiration
Access tokens expire after the time specified in the expires_in field (typically 3600 seconds / 1 hour).
Best practices:
- Store the token and its expiration time
- Check if the token is expired before each request
- Request a new token when the current one expires or is about to expire
- Add a buffer (e.g., refresh 5 minutes before actual expiration)
Example: Token Management in Python
from datetime import datetime, timedelta
class TokenManager:
def __init__(self, client_id, private_key, token_endpoint):
self.client_id = client_id
self.private_key = private_key
self.token_endpoint = token_endpoint
self.access_token = None
self.expires_at = None
def get_access_token(self):
# Return cached token if still valid
if self.access_token and datetime.utcnow() < self.expires_at:
return self.access_token
# Request new token
client_assertion = self._build_jwt()
response = requests.post(self.token_endpoint, data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": client_assertion,
"scope": "DsaApi"
})
response.raise_for_status()
token_response = response.json()
self.access_token = token_response["access_token"]
# Set expiration with 5-minute buffer
expires_in = token_response["expires_in"]
self.expires_at = datetime.utcnow() + timedelta(seconds=expires_in - 300)
return self.access_token
def _build_jwt(self):
# JWT building logic from Step 1
now = int(time.time())
payload = {
"iss": self.client_id,
"sub": self.client_id,
"aud": self.token_endpoint,
"jti": str(uuid.uuid4()),
"iat": now,
"nbf": now,
"exp": now + 300
}
return jwt.encode(payload, self.private_key, algorithm="RS384")
# Usage
token_manager = TokenManager(client_id, private_key, token_endpoint)
access_token = token_manager.get_access_token()Optional: Use DPoP for Enhanced Security
The API supports DPoP (Demonstration of Proof-of-Possession) for enhanced security. DPoP binds access tokens to a specific key pair, preventing token theft.
DPoP Token Request
Include a DPoP proof JWT in the DPoP header:
POST /connect/token HTTP/1.1
Host: auth-server.surescripts.net
Content-Type: application/x-www-form-urlencoded
DPoP: <dpop-proof-jwt>
grant_type=client_credentials
&client_id=your-client-id
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=<your-signed-jwt>
&scope=DsaApi
&dpop_jkt=<jwk-thumbprint>DPoP Proof Structure
Header:
{
"typ": "dpop+jwt",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"n": "<base64url-modulus>",
"e": "<base64url-exponent>"
}
}Payload:
{
"htu": "ENVIRONMENT_AUTH_URL/connect/token",
"htm": "POST",
"iat": 1706572800,
"jti": "unique-id-here"
}DPoP API Request
When using a DPoP-bound token, include the DPoP proof in API requests:
GET /api/v0/odata/PrescribedMedicationData HTTP/1.1
Host: ENVIRONMENT_DATA_SOLUTIONS_URL
Authorization: DPoP <access-token>
DPoP: <dpop-proof-jwt>The DPoP proof for resource requests must include the ath claim (access token hash):
{
"htu": "ENVIRONMENT_DATA_SOLUTIONS_URL/api/v0/odata/PrescribedMedicationData",
"htm": "GET",
"iat": 1706572800,
"jti": "unique-id-here",
"ath": "<base64url-sha256-of-access-token>"
}For detailed DPoP implementation, see the Postman Collection README.
Troubleshooting
"invalid_client" Error
Cause: Client ID not found or public key mismatch
Solutions:
- Verify your Client ID is correct
- Ensure your public key is registered with the auth server
- Confirm the private key matches the registered public key
"invalid_grant" Error
Cause: JWT assertion validation failed
Solutions:
- Check JWT expiration (exp claim) - ensure it's in the future
- Verify aud claim matches the exact token endpoint URL
- Ensure iss and sub both contain your Client ID
- Confirm JWT is signed with RS384 algorithm
- Check that JWT jti is unique
401 Unauthorized on API Requests
Cause: Invalid or expired access token
Solutions:
- Verify the token hasn't expired
- Check that you're using Bearer token type
- Ensure the token was requested with scope=DsaApi
- Confirm the Authorization header format: Bearer <token>
403 Forbidden on API Requests
Cause: Valid token but insufficient permissions
Solutions:
- Verify your client has purchased the product/package
- Check if the columns you're selecting are in your package
- Ensure you're including a valid PARTITION_DAY filter
- Review package filter restrictions
Token Request Timeout
Cause: Network connectivity or server issues
Solutions:
- Check network connectivity to the auth server
- Verify the auth server URL is correct
- Check firewall rules allow outbound HTTPS
Environment URLs
Environment | Auth Server URL | Data Solutions API URL |
|---|---|---|
Production | https://auth-server.surescripts.net | https://data-solutions-api.surescripts.net |
Security Best Practices
- Protect Private Keys
- Never commit private keys to source control
- Store keys securely (e.g., environment variables, key vaults)
- Use different keys for different environments
- Token Storage
- Store tokens securely in memory or encrypted storage
- Never log or expose tokens in error messages
- Clear tokens when they expire
- JWT Claims
- Always use unique jti values (UUIDs)
- Keep expiration times short (5 minutes recommended)
- Validate token expiration before use
- HTTPS Only
- Always use HTTPS for token requests and API calls
- Verify SSL/TLS certificates
- Error Handling
- Implement retry logic for transient failures
- Log authentication failures for monitoring
- Don't expose sensitive details in client-facing errors
Step 6: Download Data Files (Optional Formats)
The Data Solutions API supports optional file download formats that may be enabled for your data package. Available formats depend on your package configuration.
Supported Formats
- CSV – Human‑readable format for simple downloads and manual review
- Parquet – Binary, columnar format optimized for analytics and large‑scale processing
Note: File format availability is controlled at the package level by Surescripts. If a format is not enabled for your package, it is not available for download.
Before You Begin
Confirm that:
- Your Client ID is configured with a package that supports the desired file format
- You have completed authentication in Step 5
- You have a valid PARTITION_DAY value
File format selection does not affect query construction or authentication.
Download CSV Data (For Enabled CSV Packages Only)
Send a request to the following endpoint to download CSV data using a signed URL. The signed URL is active for 5 minutes and is only available is you are enabled for CSV downloads.
Prescribed Medication Data:
/api/v0/customer/product/PrescribedMedicationData/csv/signedurls/{PARTITION_DAY}Download Parquet Data (For Enabled Parquet Packages Only)
Send a request to the following endpoint to download Parquet data using a signed URL. The signed URL is active for 5 minutes and is only available is you are enabled for Parquet downloads.
Prescribed Medication Data:
/api/v0/customer/product/PrescribedMedicationData/parquet/signedurls/{PARTITION_DAY}