Authentication
Authentication Guide
Section titled “Authentication Guide”The Quickli Public API uses RSA signature-based authentication. Each request must be signed using your private key, and the signature is verified server-side using your registered public key.
Required Headers
Section titled “Required Headers”All API requests require the following authentication headers:
| Header | Description | Example |
|---|---|---|
X-Auth-Client-ID | Your client identifier | Loan Market Group |
X-Auth-Access-Token | UUID access token (grant identifier) | abc123-uuid-token |
X-Auth-Timestamp | ISO 8601 timestamp | 2025-11-19T10:30:00.000Z |
X-Auth-Nonce | Random UUID for replay protection | 550e8400-e29b-41d4-a716-446655440000 |
X-Auth-Signature | Base64-encoded RSA-SHA256 signature | dGhpcyBpcyBhIHNpZ25hdHVyZQ== |
Example Request
Section titled “Example Request”curl -X GET https://api.quickli.com/api/v1/user \ -H "X-Auth-Client-ID: Loan Market Group" \ -H "X-Auth-Access-Token: abc123-uuid-token" \ -H "X-Auth-Timestamp: 2025-11-19T10:30:00.000Z" \ -H "X-Auth-Nonce: 550e8400-e29b-41d4-a716-446655440000" \ -H "X-Auth-Signature: dGhpcyBpcyBhIHNpZ25hdHVyZQ=="How to Get Credentials
Section titled “How to Get Credentials”Contact Quickli to obtain:
- Client ID - Your unique client identifier
- Access Token - UUID token scoped to your organization and teams
You will also need to:
- Generate an RSA key pair (2048-bit or higher recommended)
- Share your public key with Quickli for registration
- Keep your private key secure for signing requests
Generating RSA Keys
Section titled “Generating RSA Keys”# Generate private keyopenssl genrsa -out private_key.pem 2048
# Extract public keyopenssl rsa -in private_key.pem -pubout -out public_key.pemRequest Signing
Section titled “Request Signing”Overview
Section titled “Overview”Each request must be signed to verify authenticity and prevent tampering. The signing process:
- Create a canonical request string
- Hash the request body (SHA-256)
- Sign the canonical request with your private key (RSA-SHA256)
- Base64-encode the signature
Canonical Request Format
Section titled “Canonical Request Format”The canonical request is constructed as:
{METHOD}\n{PATH}\n{TIMESTAMP}\n{NONCE}\n{BODY_HASH}Where:
METHOD- HTTP method (GET, POST, PUT, DELETE)PATH- Request path (e.g.,/api/v1/user)TIMESTAMP- ISO 8601 timestamp from theX-Auth-TimestampheaderNONCE- UUID from theX-Auth-NonceheaderBODY_HASH- SHA-256 hash of the request body (hex-encoded)
Body Hashing Rules
Section titled “Body Hashing Rules”- Empty body (GET requests): Hash an empty string
"" - JSON body: Hash the JSON string as-is
- Empty object
{}: Hash an empty string""(not"{}")
Signing Example (TypeScript)
Section titled “Signing Example (TypeScript)”import crypto from 'crypto';
function hashRequestBody(body: string): string { return crypto.createHash('sha256').update(body).digest('hex');}
function createCanonicalRequest( method: string, path: string, timestamp: string, nonce: string, bodyHash: string): string { return `${method}\n${path}\n${timestamp}\n${nonce}\n${bodyHash}`;}
function signRequest(canonicalRequest: string, privateKey: string): string { const sign = crypto.createSign('RSA-SHA256'); sign.update(canonicalRequest); return sign.sign(privateKey, 'base64');}
// Usagefunction generateAuthHeaders( clientId: string, accessToken: string, method: string, path: string, body: unknown, privateKey: string): Record<string, string> { const timestamp = new Date().toISOString(); const nonce = crypto.randomUUID();
// Handle empty body let bodyString: string; if (!body || (typeof body === 'object' && Object.keys(body).length === 0)) { bodyString = ''; } else { bodyString = typeof body === 'string' ? body : JSON.stringify(body); }
const bodyHash = hashRequestBody(bodyString); const canonicalRequest = createCanonicalRequest(method, path, timestamp, nonce, bodyHash); const signature = signRequest(canonicalRequest, privateKey);
return { 'Content-Type': 'application/json', 'X-Auth-Client-ID': clientId, 'X-Auth-Access-Token': accessToken, 'X-Auth-Timestamp': timestamp, 'X-Auth-Nonce': nonce, 'X-Auth-Signature': signature, };}Signing Example (Python)
Section titled “Signing Example (Python)”import hashlibimport base64import uuidfrom datetime import datetimefrom cryptography.hazmat.primitives import hashes, serializationfrom cryptography.hazmat.primitives.asymmetric import paddingimport json
def hash_request_body(body: str) -> str: return hashlib.sha256(body.encode()).hexdigest()
def create_canonical_request(method: str, path: str, timestamp: str, nonce: str, body_hash: str) -> str: return f"{method}\n{path}\n{timestamp}\n{nonce}\n{body_hash}"
def sign_request(canonical_request: str, private_key_pem: str) -> str: private_key = serialization.load_pem_private_key( private_key_pem.encode(), password=None ) signature = private_key.sign( canonical_request.encode(), padding.PKCS1v15(), hashes.SHA256() ) return base64.b64encode(signature).decode()
def generate_auth_headers( client_id: str, access_token: str, method: str, path: str, body: dict | None, private_key: str) -> dict: timestamp = datetime.utcnow().isoformat() + 'Z' nonce = str(uuid.uuid4())
# Handle empty body if not body or (isinstance(body, dict) and len(body) == 0): body_string = '' else: body_string = json.dumps(body) if isinstance(body, dict) else body
body_hash = hash_request_body(body_string) canonical_request = create_canonical_request(method, path, timestamp, nonce, body_hash) signature = sign_request(canonical_request, private_key)
return { 'Content-Type': 'application/json', 'X-Auth-Client-ID': client_id, 'X-Auth-Access-Token': access_token, 'X-Auth-Timestamp': timestamp, 'X-Auth-Nonce': nonce, 'X-Auth-Signature': signature, }Authorization Model
Section titled “Authorization Model”Organization Scoping
Section titled “Organization Scoping”Each API access grant is scoped to one organization. All operations are limited to data within that organization.
{ userId: "user123", organizationId: "org456", // Single org per token teamIds: ["team1", "team2", "team3"] // Multiple teams allowed}Team-Level Access
Section titled “Team-Level Access”Within your organization, you can access multiple teams. The API validates team access for operations like:
- Creating scenarios (
teamIdparameter) - Accessing scenarios (must belong to accessible team)
- Listing user teams
Example: Getting your accessible teams
You can view all accessible teams and the org ID scoped to the current access token by using the GET /user Endpoint
curl -X GET https://api.quickli.com/api/v1/user \ -H "X-Auth-Client-ID: {clientId}" \ -H "X-Auth-Access-Token: {accessToken}" \ -H "X-Auth-Timestamp: {timestamp}" \ -H "X-Auth-Nonce: {nonce}" \ -H "X-Auth-Signature: {signature}"Response:
{ "data": { "email": "broker@example.com", "teams": [ { "id": "507f1f77bcf86cd799439011", "name": "Team Alpha" }, { "id": "507f191e810c19729de860ea", "name": "Team Beta" } ] }}Permission Checks
Section titled “Permission Checks”The API performs authorization checks for:
✅ User access - Is the user valid and active? ✅ Organization access - Does the grant allow access to this org? ✅ Team access - Does the grant allow access to this team? ✅ Lender access - Does the user have permission for this lender?
Security Best Practices
Section titled “Security Best Practices”🔒 Protect Your Private Key
Section titled “🔒 Protect Your Private Key”- Never commit private keys to version control
- Store keys in environment variables or secret management tools
- Use different keys for development/staging/production
- Set appropriate file permissions (600 for private keys)
🕐 Timestamp Validation
Section titled “🕐 Timestamp Validation”- Requests with timestamps too far in the past or future will be rejected
- Ensure your server clock is synchronized (use NTP)
- Timestamp window is typically ±5 minutes
🔄 Nonce Usage
Section titled “🔄 Nonce Usage”- Each nonce should be unique per request
- Used to prevent replay attacks
- Use UUID v4 for guaranteed uniqueness
🚫 Don’t Expose Keys
Section titled “🚫 Don’t Expose Keys”- Never log private keys
- Don’t include keys in error messages
- Mask keys in debugging output
- Use secure HTTPS connections only
Common Authentication Errors
Section titled “Common Authentication Errors”401 Unauthorized - Invalid Signature
Section titled “401 Unauthorized - Invalid Signature”Causes:
- Signature doesn’t match the canonical request
- Wrong private key used
- Body hash mismatch (especially with empty bodies)
- Incorrect canonical request format
Example response:
{ "error": { "code": "INVALID_SIGNATURE", "message": "Request signature verification failed", "timestamp": "2025-11-19T10:30:00.000Z" }}Solution: Verify your signing implementation matches the canonical request format exactly. Pay special attention to empty body handling.
401 Unauthorized - Invalid Client
Section titled “401 Unauthorized - Invalid Client”Causes:
- Unknown client identifier
- Client not registered in the system
Example response:
{ "error": { "code": "UNAUTHORIZED", "message": "Invalid client identifier", "timestamp": "2025-11-19T10:30:00.000Z" }}Solution: Verify your client ID is correct and registered with Quickli.
401 Unauthorized - Missing Headers
Section titled “401 Unauthorized - Missing Headers”Causes:
- Missing one or more required authentication headers
- Invalid or expired access token
- User not found
Example response:
{ "error": { "code": "UNAUTHORIZED", "message": "Missing required authentication headers", "timestamp": "2025-11-19T10:30:00.000Z" }}Solution: Ensure all five authentication headers are present and valid.
403 Forbidden
Section titled “403 Forbidden”Causes:
- User doesn’t have access to the requested team
- User doesn’t have permission for the requested lender
- Attempting to access resources outside your organization
Example response:
{ "error": { "code": "FORBIDDEN", "message": "User does not have access to this team", "timestamp": "2025-11-19T10:30:00.000Z" }}Solution: Check that you’re using the correct teamId from your /api/v1/user response.
404 Not Found
Section titled “404 Not Found”Causes:
- User is attempting to access a piece of data that belongs to a team they are not a part of
Example response:
{ "error": { "code": "NOT_FOUND", "message": "Resource not found", "timestamp": "2025-11-19T10:30:00.000Z" }}Solution: Check that you’re using the correct resource ID and have access to the team that owns it.
Testing Your Authentication
Section titled “Testing Your Authentication”Use the /api/v1/user endpoint to verify your authentication setup:
const headers = generateAuthHeaders( 'Your Client ID', 'your-access-token', 'GET', '/api/v1/user', {}, privateKey);
const response = await fetch('https://api.quickli.com/api/v1/user', { method: 'GET', headers});
if (response.ok) { const data = await response.json(); console.log('Authentication successful:', data.data.email);} else { const error = await response.json(); console.error('Authentication failed:', error.error.message);}Success response (200):
{ "data": { "email": "broker@example.com", "teams": [...] }, "meta": { "timestamp": "2025-11-19T10:30:00.000Z", "version": "v1" }}Failure response (401):
{ "error": { "code": "INVALID_SIGNATURE", "message": "Request signature verification failed", "timestamp": "2025-11-19T10:30:00.000Z" }}Debugging Signature Issues
Section titled “Debugging Signature Issues”If you’re getting INVALID_SIGNATURE errors:
- Check empty body handling: For GET requests or empty objects, hash an empty string
"" - Verify canonical request format: Ensure newlines are
\nnot\r\n - Check timestamp format: Must be ISO 8601 (e.g.,
2025-11-19T10:30:00.000Z) - Verify path: Must match exactly, including leading slash
- Check key format: Ensure PEM format with proper headers
Debug Logging
Section titled “Debug Logging”console.log('Canonical request:', canonicalRequest);console.log('Body hash:', bodyHash);console.log('Signature:', signature);Next Steps
Section titled “Next Steps”Last updated: 2025-11-19