API Reference
The Qwick Cert REST API. All endpoints live under /api/v1 and return a consistent JSON envelope.
Base URL
https://app.qwickcert.com/api/v1
All endpoints described below are relative to this base. For example, POST /sign-digest means POST https://app.qwickcert.com/api/v1/sign-digest.
Authentication
Every API request must include an Authorization header with either a user JWT (from the CLI login flow) or an API key.
User JWT
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
API Key
Authorization: Bearer qwick_ak_xxxxxxxxxxxxxxxxxxxx
API key permissions
API keys are scoped with specific permissions. A key with only sign permission can call /sign-digest but not /apikeys. Available permissions: sign, read_audit.
Required headers
| Header | Description |
|---|---|
Authorization | Bearer token (JWT or API key) |
Content-Type | application/json for POST/PUT requests |
Response headers
| Header | Description |
|---|---|
X-Qwick-CLI-Update | CLI update status: required, recommended, or optional |
X-RateLimit-Limit | Maximum requests allowed in the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the rate limit window resets |
Response format
Every response is a JSON object with a consistent envelope. The top-level shape depends on whether the request succeeded or failed.
Success (2xx)
{
"data": {
"operation_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed"
},
"request_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"api_version": "v1"
}Error (4xx / 5xx)
{
"error": {
"code": "QWICK_AUTH_301",
"message": "Access token expired or invalid",
"details": {},
"docs": "https://docs.qwickcert.com/errors/QWICK_AUTH_301"
},
"request_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"api_version": "v1"
}| Field | Type | Description |
|---|---|---|
data | object | Response payload (present on success, absent on error) |
error | object | Error details (present on failure, absent on success) |
request_id | uuid | Unique ID for this request (useful for support) |
api_version | string | Always "v1" |
Signing
/api/v1/sign-digestSign a pre-computed Authenticode digest. The server signs the hash via Azure Trusted Signing and returns a PKCS#7 signature blob with an RFC 3161 timestamp. Your binary never leaves your machine.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
digest | string | Yes | Base64-encoded SHA-256 Authenticode digest (32 bytes) |
algorithm | string | Yes | Hash algorithm used. Currently only "sha256" |
file_name | string | Yes | Original file name (for audit trail) |
file_size | number | Yes | Original file size in bytes |
organization_slug | string | Yes | Target organization slug |
batch_id | uuid | No | Group multiple sign requests under one batch |
reason | string | No | Signing reason (required if org policy enforces it) |
client_channel | string | No | Source: "cli", "github_action", "api" |
client_version | string | No | CLI or SDK version string |
Example request
curl -X POST https://app.qwickcert.com/api/v1/sign-digest \
-H "Authorization: Bearer qwick_ak_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"digest": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=",
"algorithm": "sha256",
"file_name": "MyApp.exe",
"file_size": 2048576,
"organization_slug": "acme-corp",
"client_channel": "cli",
"client_version": "1.2.0"
}'Success response (200)
{
"data": {
"operation_id": "550e8400-e29b-41d4-a716-446655440000",
"signature": "MIIRoQYJKoZIhvcNAQcCoIIRkjCCEY4C...",
"certificate_chain": [
"MIIFwTCCBGmgAwIBAgITMwAAA...",
"MIIFrDCCBJSgAwIBAgIQB..."
],
"timestamp": "2026-03-09T14:23:45Z",
"signed_at": "2026-03-09T14:23:44.892Z"
},
"request_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"api_version": "v1"
}Error responses
| Status | Code | Meaning |
|---|---|---|
| 400 | QWICK_API_600 | Invalid request body (missing digest, bad algorithm, etc.) |
| 401 | QWICK_AUTH_301 | Token expired or invalid |
| 403 | QWICK_AUTH_303 | Insufficient role (requires signer or higher) |
| 403 | QWICK_BILLING_700 | Plan limit reached |
| 429 | QWICK_BILLING_702 | Free tier annual signing limit reached |
| 503 | QWICK_SIGN_102 | Azure credential still provisioning (retry in a few seconds) |
| 502 | QWICK_SIGN_103 | Azure signing service error |
Signing Operations
/api/v1/signing-operationsList signing operations for an organization with optional filters. Powers the audit dashboard.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
organization_slug | string | Yes | Organization slug |
status | string | No | Filter: pending, signed, completed, embed_failed, failed, timeout, abandoned |
source | string | No | Filter by source: cli, github_action, dashboard, api |
batch_id | uuid | No | Filter by signing batch ID |
actor_user_id | uuid | No | Filter by the user who initiated signing |
date_from | ISO 8601 | No | Lower bound on created_at |
date_to | ISO 8601 | No | Upper bound on created_at |
page | number | No | Page number (default: 1) |
page_size | number | No | Items per page (default: 50, max: 100) |
Example request
curl "https://app.qwickcert.com/api/v1/signing-operations?organization_slug=acme-corp&status=completed&page=1&page_size=10" \ -H "Authorization: Bearer qwick_ak_xxxxxxxxxxxx"
Success response (200)
{
"data": {
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"organization_id": "org-uuid",
"actor_user_id": "user-uuid",
"actor_email": "dev@acme.com",
"file_name": "MyApp.exe",
"file_size": 2048576,
"digest_algorithm": "sha256",
"status": "completed",
"source": "cli",
"batch_id": null,
"signed_at": "2026-03-09T14:23:44.892Z",
"created_at": "2026-03-09T14:23:42.100Z"
}
],
"page": 1,
"page_size": 10,
"total": 1
},
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"api_version": "v1"
}/api/v1/signing-operations/:idGet details for a single signing operation by ID.
Example request
curl "https://app.qwickcert.com/api/v1/signing-operations/550e8400-e29b-41d4-a716-446655440000?organization_slug=acme-corp" \ -H "Authorization: Bearer qwick_ak_xxxxxxxxxxxx"
Success response (200)
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"organization_id": "org-uuid",
"actor_user_id": "user-uuid",
"actor_email": "dev@acme.com",
"file_name": "MyApp.exe",
"file_size": 2048576,
"file_hash": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=",
"digest_algorithm": "sha256",
"status": "completed",
"source": "cli",
"client_channel": "cli",
"client_version": "1.2.0",
"batch_id": null,
"signed_at": "2026-03-09T14:23:44.892Z",
"duration_ms": 2792,
"created_at": "2026-03-09T14:23:42.100Z"
},
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"api_version": "v1"
}/api/v1/signing-operations/exportExport signing operations as a CSV file. Accepts the same filters as the list endpoint.
Example request
curl "https://app.qwickcert.com/api/v1/signing-operations/export?organization_slug=acme-corp&date_from=2026-01-01" \ -H "Authorization: Bearer qwick_ak_xxxxxxxxxxxx" \ -o signing-audit.csv
Returns a text/csv response with columns: id, file_name, file_size, status, source, actor_email, signed_at, created_at.
API Keys
/api/v1/apikeysList all API keys for an organization. Returns metadata only (the full key is shown once at creation).
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
organization_slug | string | Yes | Organization slug |
Example request
curl "https://app.qwickcert.com/api/v1/apikeys?organization_slug=acme-corp" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
Success response (200)
{
"data": {
"keys": [
{
"id": "key-uuid",
"name": "CI Pipeline",
"prefix": "qwick_ak_abc1",
"permissions": ["sign", "read_audit"],
"expires_at": "2026-09-09T00:00:00Z",
"days_until_expiry": 183,
"last_used_at": "2026-03-08T10:30:00Z",
"use_count": 142,
"created_at": "2026-03-01T12:00:00Z"
}
]
},
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"api_version": "v1"
}/api/v1/apikeysCreate a new API key. Returns the full key exactly once — store it securely.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
organization_slug | string | Yes | Organization slug |
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable name for the key |
permissions | string[] | Yes | Array of permissions: "sign", "read_audit" |
expires_in_months | number | No | Months until expiry (default: 6, max: 24) |
Example request
curl -X POST "https://app.qwickcert.com/api/v1/apikeys?organization_slug=acme-corp" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"name": "GitHub Actions",
"permissions": ["sign"],
"expires_in_months": 6
}'Success response (201)
{
"data": {
"id": "key-uuid",
"name": "GitHub Actions",
"key": "qwick_ak_xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"prefix": "qwick_ak_xxxx",
"permissions": ["sign"],
"expires_at": "2026-09-09T00:00:00Z",
"created_at": "2026-03-09T15:00:00Z"
},
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"api_version": "v1"
}Store the key immediately
The full key value is returned only once. It cannot be retrieved again — only the prefix is stored for identification.
/api/v1/apikeys/:id/revokeRevoke an API key immediately. The key will stop working within seconds.
Example request
curl -X POST "https://app.qwickcert.com/api/v1/apikeys/key-uuid/revoke?organization_slug=acme-corp" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
Success response (200)
{
"data": {
"id": "key-uuid",
"revoked_at": "2026-03-09T15:30:00Z"
},
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"api_version": "v1"
}Billing
/api/v1/billing/summaryGet the current subscription state, usage counts, and active discount for an organization.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
organization_slug | string | Yes | Organization slug |
Example request
curl "https://app.qwickcert.com/api/v1/billing/summary?organization_slug=acme-corp" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
Success response (200)
{
"data": {
"plan": "pro",
"status": "active",
"current_period_end": "2026-04-09T00:00:00Z",
"cancel_at_period_end": false,
"limits": {
"members": 10,
"signing_operations": "unlimited"
},
"usage": {
"members": 3,
"signing_operations_this_period": 847
},
"discount": {
"name": "Annual 20%",
"percent_off": 20,
"ends_at": "2027-03-09T00:00:00Z"
}
},
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"api_version": "v1"
}Rate limits
API requests are rate-limited per organization to protect service quality. Limits are returned in response headers.
| Plan | Rate limit | Signing concurrency |
|---|---|---|
| Free | 60 requests/min | 1 concurrent session |
| Pro | 300 requests/min | 2 concurrent sessions |
| Enterprise | 1000 requests/min | Custom |
When you exceed the rate limit, the API returns 429 Too Many Requests with a Retry-After header indicating seconds to wait.
Rate limit error
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1741537424
{
"error": {
"code": "QWICK_API_604",
"message": "Rate limit exceeded",
"details": { "retry_after": 12 },
"docs": "https://docs.qwickcert.com/errors/QWICK_API_604"
},
"request_id": "...",
"api_version": "v1"
}Error codes
All errors use structured QWICK_* codes grouped by category. Each error includes a docs URL for resolution steps.
Infrastructure (000-099)
| Code | HTTP | Description |
|---|---|---|
QWICK_INFRA_000 | 500 | Internal server error |
Signing (100-199)
| Code | HTTP | Description |
|---|---|---|
QWICK_SIGN_101 | 429 | Queue timeout exceeded |
QWICK_SIGN_102 | 503 | Failed to acquire signing credential (Azure still provisioning) |
QWICK_SIGN_103 | 502 | Azure signing service returned an error |
QWICK_SIGN_104 | 502 | Transient Azure error (safe to retry) |
Authentication (300-399)
| Code | HTTP | Description |
|---|---|---|
QWICK_AUTH_300 | 401 | Invalid credentials |
QWICK_AUTH_301 | 401 | Access token expired or invalid |
QWICK_AUTH_302 | 401 | Refresh token revoked or expired |
QWICK_AUTH_303 | 403 | Insufficient role or permission |
QWICK_AUTH_304 | 401 | API key revoked or expired |
QWICK_AUTH_305 | 429 | Too many authentication attempts |
Policy (400-499)
| Code | HTTP | Description |
|---|---|---|
QWICK_POLICY_400 | 403 | 2FA required by organization policy |
QWICK_POLICY_401 | 403 | IP address not in allowlist |
QWICK_POLICY_402 | 409 | Approval pending (approval workflow enabled) |
QWICK_POLICY_403 | 429 | Per-user rate limit exceeded |
QWICK_POLICY_404 | 403 | Outside allowed signing hours |
QWICK_POLICY_407 | 403 | Source not allowed by policy |
QWICK_POLICY_408 | 403 | File extension not allowed by policy |
QWICK_POLICY_409 | 413 | File size exceeds policy limit |
QWICK_POLICY_415 | 403 | Organization signing is locked down |
API (600-699)
| Code | HTTP | Description |
|---|---|---|
QWICK_API_600 | 400 | Request validation failed |
QWICK_API_601 | 409 | Resource conflict |
QWICK_API_602 | 404 | Resource not found in tenant scope |
QWICK_API_604 | 429 | Rate limit exceeded |
Billing (700-799)
| Code | HTTP | Description |
|---|---|---|
QWICK_BILLING_700 | 403 | Plan limit reached |
QWICK_BILLING_701 | 403 | Subscription cancelled — signing disabled |
QWICK_BILLING_702 | 429 | Free tier annual signing limit reached |