Skip to content

Authentication

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.

All API requests require the following authentication headers:

HeaderDescriptionExample
X-Auth-Client-IDYour client identifierLoan Market Group
X-Auth-Access-TokenUUID access token (grant identifier)abc123-uuid-token
X-Auth-TimestampISO 8601 timestamp2025-11-19T10:30:00.000Z
X-Auth-NonceRandom UUID for replay protection550e8400-e29b-41d4-a716-446655440000
X-Auth-SignatureBase64-encoded RSA-SHA256 signaturedGhpcyBpcyBhIHNpZ25hdHVyZQ==
Terminal window
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=="

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:

  1. Generate an RSA key pair (2048-bit or higher recommended)
  2. Share your public key with Quickli for registration
  3. Keep your private key secure for signing requests
Terminal window
# Generate private key
openssl genrsa -out private_key.pem 2048
# Extract public key
openssl rsa -in private_key.pem -pubout -out public_key.pem

Each request must be signed to verify authenticity and prevent tampering. The signing process:

  1. Create a canonical request string
  2. Hash the request body (SHA-256)
  3. Sign the canonical request with your private key (RSA-SHA256)
  4. Base64-encode the signature

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 the X-Auth-Timestamp header
  • NONCE - UUID from the X-Auth-Nonce header
  • BODY_HASH - SHA-256 hash of the request body (hex-encoded)
  • Empty body (GET requests): Hash an empty string ""
  • JSON body: Hash the JSON string as-is
  • Empty object {}: Hash an empty string "" (not "{}")
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');
}
// Usage
function 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,
};
}
import hashlib
import base64
import uuid
from datetime import datetime
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
import 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,
}

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
}

Within your organization, you can access multiple teams. The API validates team access for operations like:

  • Creating scenarios (teamId parameter)
  • 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

Terminal window
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"
}
]
}
}

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?

  • 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)
  • 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
  • Each nonce should be unique per request
  • Used to prevent replay attacks
  • Use UUID v4 for guaranteed uniqueness
  • Never log private keys
  • Don’t include keys in error messages
  • Mask keys in debugging output
  • Use secure HTTPS connections only

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.

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.

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.

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.

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.

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"
}
}

If you’re getting INVALID_SIGNATURE errors:

  1. Check empty body handling: For GET requests or empty objects, hash an empty string ""
  2. Verify canonical request format: Ensure newlines are \n not \r\n
  3. Check timestamp format: Must be ISO 8601 (e.g., 2025-11-19T10:30:00.000Z)
  4. Verify path: Must match exactly, including leading slash
  5. Check key format: Ensure PEM format with proper headers
console.log('Canonical request:', canonicalRequest);
console.log('Body hash:', bodyHash);
console.log('Signature:', signature);

Last updated: 2025-11-19