Skip to main content

BIO-QMS API Documentation

Overview

The BIO-QMS Platform provides a comprehensive RESTful API for integrating quality management, regulatory compliance, and document control workflows into your biosciences applications. Built on NestJS with Prisma ORM and PostgreSQL, the API follows industry best practices for security, validation, and audit trailing.

Base URL: https://api.bio-qms.com/v1

API Characteristics:

  • RESTful design with resource-based endpoints
  • OpenAPI 3.0 specification
  • OAuth2 + JWT authentication
  • Comprehensive webhook system
  • Full audit trail for all operations
  • Rate limiting: 1000 requests/hour per API key
  • TLS 1.3 required for all connections

Compliance Features:

  • FDA 21 CFR Part 11 compliant electronic signatures
  • HIPAA-compliant data handling with encryption at rest and in transit
  • SOC 2 Type II controls for audit logging and access management
  • Immutable audit trails with cryptographic verification

Table of Contents

  1. F.2.1: OpenAPI Specification Documentation
  2. F.2.2: API Authentication Guide
  3. F.2.3: Webhook Integration Guide
  4. F.2.4: Integration Cookbook with Code Examples

F.2.1: OpenAPI Specification Documentation

Accessing API Documentation

The BIO-QMS API provides multiple documentation interfaces:

InterfaceURLPurpose
Swagger UI/api/docsInteractive API explorer with try-it-out functionality
ReDoc/api/referenceClean, responsive API reference documentation
OpenAPI JSON/api/docs-jsonRaw OpenAPI 3.0 specification
OpenAPI YAML/api/docs-yamlYAML format OpenAPI specification

Versioning Strategy

The API supports multiple concurrent versions:

/api/v1/documents          # Version 1 (stable)
/api/v2/documents # Version 2 (current)
/api/beta/documents # Beta features

Version Lifecycle:

  • Each version supported for minimum 24 months after deprecation
  • Deprecation warnings sent via Deprecation HTTP header
  • Version sunset dates announced 12 months in advance

Auto-Generation from NestJS

OpenAPI documentation is automatically generated from NestJS decorators:

// Example: Document Controller with OpenAPI Decorators
import { Controller, Get, Post, Body, Param, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBearerAuth } from '@nestjs/swagger';

@ApiTags('documents')
@Controller('api/v1/documents')
@ApiBearerAuth()
export class DocumentsController {

@Get()
@ApiOperation({
summary: 'List all documents',
description: 'Retrieve a paginated list of documents with optional filtering'
})
@ApiResponse({
status: 200,
description: 'Documents retrieved successfully',
type: [DocumentDto]
})
@ApiResponse({ status: 401, description: 'Unauthorized' })
async findAll(@Query() query: DocumentQueryDto): Promise<PaginatedResponse<DocumentDto>> {
// Implementation
}

@Post()
@ApiOperation({ summary: 'Create a new document' })
@ApiResponse({
status: 201,
description: 'Document created successfully',
type: DocumentDto
})
@ApiResponse({ status: 400, description: 'Invalid request body' })
async create(@Body() createDto: CreateDocumentDto): Promise<DocumentDto> {
// Implementation
}
}

DTO Decorators for Schema Generation

// Document DTO with OpenAPI Property Decorators
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsEnum, IsOptional, IsUUID, IsDateString } from 'class-validator';

export class CreateDocumentDto {

@ApiProperty({
description: 'Document title',
example: 'SOP-001: Manufacturing Process Validation',
maxLength: 255
})
@IsString()
title: string;

@ApiProperty({
description: 'Document type',
enum: ['SOP', 'PROTOCOL', 'REPORT', 'FORM', 'SPECIFICATION'],
example: 'SOP'
})
@IsEnum(['SOP', 'PROTOCOL', 'REPORT', 'FORM', 'SPECIFICATION'])
type: string;

@ApiPropertyOptional({
description: 'Document description',
example: 'This SOP defines the manufacturing validation process...'
})
@IsOptional()
@IsString()
description?: string;

@ApiProperty({
description: 'Department ID',
format: 'uuid',
example: '550e8400-e29b-41d4-a716-446655440000'
})
@IsUUID()
departmentId: string;

@ApiPropertyOptional({
description: 'Effective date',
format: 'date-time',
example: '2026-03-01T00:00:00Z'
})
@IsOptional()
@IsDateString()
effectiveDate?: string;
}

export class DocumentDto {

@ApiProperty({ format: 'uuid' })
id: string;

@ApiProperty()
title: string;

@ApiProperty({ enum: ['SOP', 'PROTOCOL', 'REPORT', 'FORM', 'SPECIFICATION'] })
type: string;

@ApiProperty({ enum: ['DRAFT', 'REVIEW', 'APPROVED', 'EFFECTIVE', 'OBSOLETE'] })
status: string;

@ApiProperty()
version: number;

@ApiPropertyOptional()
description?: string;

@ApiProperty()
createdBy: string;

@ApiProperty({ format: 'date-time' })
createdAt: Date;

@ApiProperty({ format: 'date-time' })
updatedAt: Date;

@ApiPropertyOptional({ format: 'date-time' })
approvedAt?: Date;

@ApiPropertyOptional()
approvedBy?: string;
}

OpenAPI Schema Fragments

1. Documents Endpoint

openapi: 3.0.0
info:
title: BIO-QMS API
version: 1.0.0
description: Quality Management System API for regulated biosciences
contact:
name: BIO-QMS Support
email: api-support@bio-qms.com

servers:
- url: https://api.bio-qms.com/v1
description: Production
- url: https://staging-api.bio-qms.com/v1
description: Staging

paths:
/documents:
get:
tags:
- documents
summary: List all documents
description: Retrieve a paginated list of documents with filtering and sorting
operationId: listDocuments
security:
- bearerAuth: []
parameters:
- name: page
in: query
schema:
type: integer
default: 1
minimum: 1
- name: limit
in: query
schema:
type: integer
default: 20
minimum: 1
maximum: 100
- name: status
in: query
schema:
type: string
enum: [DRAFT, REVIEW, APPROVED, EFFECTIVE, OBSOLETE]
- name: type
in: query
schema:
type: string
enum: [SOP, PROTOCOL, REPORT, FORM, SPECIFICATION]
- name: departmentId
in: query
schema:
type: string
format: uuid
- name: sortBy
in: query
schema:
type: string
enum: [createdAt, updatedAt, title, version]
default: createdAt
- name: sortOrder
in: query
schema:
type: string
enum: [asc, desc]
default: desc
responses:
'200':
description: Documents retrieved successfully
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Document'
meta:
type: object
properties:
page:
type: integer
limit:
type: integer
total:
type: integer
totalPages:
type: integer
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'429':
$ref: '#/components/responses/RateLimitExceeded'

post:
tags:
- documents
summary: Create a new document
operationId: createDocument
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateDocumentDto'
responses:
'201':
description: Document created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Document'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'

/documents/{id}:
get:
tags:
- documents
summary: Get document by ID
operationId: getDocumentById
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Document retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Document'
'404':
$ref: '#/components/responses/NotFound'

patch:
tags:
- documents
summary: Update document
operationId: updateDocument
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateDocumentDto'
responses:
'200':
description: Document updated successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Document'
'400':
$ref: '#/components/responses/BadRequest'
'404':
$ref: '#/components/responses/NotFound'

delete:
tags:
- documents
summary: Delete document (soft delete)
operationId: deleteDocument
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'204':
description: Document deleted successfully
'404':
$ref: '#/components/responses/NotFound'
'409':
description: Cannot delete document in current state

2. CAPAs Endpoint

paths:
/capas:
get:
tags:
- capas
summary: List all CAPAs
description: Retrieve corrective and preventive actions with filtering
operationId: listCapas
security:
- bearerAuth: []
parameters:
- name: status
in: query
schema:
type: string
enum: [OPEN, IN_PROGRESS, PENDING_VERIFICATION, VERIFIED, CLOSED]
- name: priority
in: query
schema:
type: string
enum: [LOW, MEDIUM, HIGH, CRITICAL]
- name: assignedTo
in: query
schema:
type: string
format: uuid
- name: dueDateFrom
in: query
schema:
type: string
format: date-time
- name: dueDateTo
in: query
schema:
type: string
format: date-time
responses:
'200':
description: CAPAs retrieved successfully
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Capa'
meta:
$ref: '#/components/schemas/PaginationMeta'

post:
tags:
- capas
summary: Create a new CAPA
operationId: createCapa
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateCapaDto'
responses:
'201':
description: CAPA created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Capa'

/capas/{id}/actions:
post:
tags:
- capas
summary: Add action to CAPA
operationId: addCapaAction
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- description
- actionType
- assignedTo
- dueDate
properties:
description:
type: string
actionType:
type: string
enum: [CORRECTIVE, PREVENTIVE]
assignedTo:
type: string
format: uuid
dueDate:
type: string
format: date-time
responses:
'201':
description: Action added successfully
content:
application/json:
schema:
$ref: '#/components/schemas/CapaAction'

3. Deviations Endpoint

paths:
/deviations:
get:
tags:
- deviations
summary: List all deviations
operationId: listDeviations
security:
- bearerAuth: []
parameters:
- name: severity
in: query
schema:
type: string
enum: [MINOR, MAJOR, CRITICAL]
- name: status
in: query
schema:
type: string
enum: [REPORTED, UNDER_INVESTIGATION, RESOLVED, CLOSED]
- name: reportedDateFrom
in: query
schema:
type: string
format: date-time
responses:
'200':
description: Deviations retrieved successfully
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Deviation'
meta:
$ref: '#/components/schemas/PaginationMeta'

post:
tags:
- deviations
summary: Report a new deviation
operationId: createDeviation
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateDeviationDto'
responses:
'201':
description: Deviation created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Deviation'

/deviations/{id}/escalate:
post:
tags:
- deviations
summary: Escalate deviation
description: Escalate a deviation to higher priority or management attention
operationId: escalateDeviation
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- reason
- escalatedTo
properties:
reason:
type: string
escalatedTo:
type: string
format: uuid
newSeverity:
type: string
enum: [MAJOR, CRITICAL]
responses:
'200':
description: Deviation escalated successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Deviation'

4. Users Endpoint

paths:
/users:
get:
tags:
- users
summary: List all users
operationId: listUsers
security:
- bearerAuth: []
parameters:
- name: role
in: query
schema:
type: string
enum: [ADMIN, QUALITY_MANAGER, OPERATOR, REVIEWER, APPROVER]
- name: departmentId
in: query
schema:
type: string
format: uuid
- name: isActive
in: query
schema:
type: boolean
responses:
'200':
description: Users retrieved successfully
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
meta:
$ref: '#/components/schemas/PaginationMeta'

post:
tags:
- users
summary: Create a new user
operationId: createUser
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserDto'
responses:
'201':
description: User created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/User'

/users/{id}:
get:
tags:
- users
summary: Get user by ID
operationId: getUserById
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: User retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/User'

5. Audit Trail Endpoint

paths:
/audit-trail:
get:
tags:
- audit-trail
summary: Query audit trail
description: Retrieve audit trail entries with advanced filtering
operationId: queryAuditTrail
security:
- bearerAuth: []
parameters:
- name: entityType
in: query
schema:
type: string
enum: [DOCUMENT, CAPA, DEVIATION, USER, TRAINING, SIGNATURE]
- name: entityId
in: query
schema:
type: string
format: uuid
- name: action
in: query
schema:
type: string
enum: [CREATE, UPDATE, DELETE, APPROVE, REJECT, SIGN, ESCALATE]
- name: userId
in: query
schema:
type: string
format: uuid
- name: dateFrom
in: query
required: true
schema:
type: string
format: date-time
- name: dateTo
in: query
required: true
schema:
type: string
format: date-time
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 50
maximum: 500
responses:
'200':
description: Audit trail retrieved successfully
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/AuditEntry'
meta:
$ref: '#/components/schemas/PaginationMeta'
verification:
type: object
description: Cryptographic verification of audit trail integrity
properties:
chainValid:
type: boolean
lastVerifiedHash:
type: string
verifiedAt:
type: string
format: date-time

/audit-trail/export:
post:
tags:
- audit-trail
summary: Export audit trail
description: Generate a tamper-evident audit trail export for regulatory submission
operationId: exportAuditTrail
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- dateFrom
- dateTo
- format
properties:
dateFrom:
type: string
format: date-time
dateTo:
type: string
format: date-time
format:
type: string
enum: [PDF, CSV, JSON]
includeSignatureProof:
type: boolean
default: true
filters:
type: object
properties:
entityTypes:
type: array
items:
type: string
userIds:
type: array
items:
type: string
format: uuid
responses:
'200':
description: Export generated successfully
content:
application/json:
schema:
type: object
properties:
downloadUrl:
type: string
format: uri
expiresAt:
type: string
format: date-time
fileSize:
type: integer
cryptographicHash:
type: string
description: SHA-256 hash of the export for integrity verification

Component Schemas

components:
schemas:
Document:
type: object
required:
- id
- title
- type
- status
- version
- createdBy
- createdAt
- updatedAt
properties:
id:
type: string
format: uuid
title:
type: string
maxLength: 255
type:
type: string
enum: [SOP, PROTOCOL, REPORT, FORM, SPECIFICATION]
status:
type: string
enum: [DRAFT, REVIEW, APPROVED, EFFECTIVE, OBSOLETE]
version:
type: integer
minimum: 1
description:
type: string
departmentId:
type: string
format: uuid
createdBy:
type: string
format: uuid
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
approvedAt:
type: string
format: date-time
approvedBy:
type: string
format: uuid
effectiveDate:
type: string
format: date-time
reviewDueDate:
type: string
format: date-time
documentNumber:
type: string
description: Auto-generated document number (e.g., DOC-2026-0001)
tags:
type: array
items:
type: string

CreateDocumentDto:
type: object
required:
- title
- type
- departmentId
properties:
title:
type: string
maxLength: 255
type:
type: string
enum: [SOP, PROTOCOL, REPORT, FORM, SPECIFICATION]
description:
type: string
departmentId:
type: string
format: uuid
effectiveDate:
type: string
format: date-time
reviewPeriodMonths:
type: integer
minimum: 1
maximum: 60
default: 12
tags:
type: array
items:
type: string

UpdateDocumentDto:
type: object
properties:
title:
type: string
maxLength: 255
description:
type: string
effectiveDate:
type: string
format: date-time
reviewPeriodMonths:
type: integer
tags:
type: array
items:
type: string

Capa:
type: object
required:
- id
- title
- status
- priority
- createdBy
- createdAt
properties:
id:
type: string
format: uuid
capaNumber:
type: string
description: Auto-generated CAPA number (e.g., CAPA-2026-0042)
title:
type: string
description:
type: string
status:
type: string
enum: [OPEN, IN_PROGRESS, PENDING_VERIFICATION, VERIFIED, CLOSED]
priority:
type: string
enum: [LOW, MEDIUM, HIGH, CRITICAL]
rootCause:
type: string
assignedTo:
type: string
format: uuid
dueDate:
type: string
format: date-time
createdBy:
type: string
format: uuid
createdAt:
type: string
format: date-time
closedAt:
type: string
format: date-time
relatedDeviationId:
type: string
format: uuid
actions:
type: array
items:
$ref: '#/components/schemas/CapaAction'

CreateCapaDto:
type: object
required:
- title
- description
- priority
- assignedTo
- dueDate
properties:
title:
type: string
description:
type: string
priority:
type: string
enum: [LOW, MEDIUM, HIGH, CRITICAL]
rootCause:
type: string
assignedTo:
type: string
format: uuid
dueDate:
type: string
format: date-time
relatedDeviationId:
type: string
format: uuid

CapaAction:
type: object
properties:
id:
type: string
format: uuid
capaId:
type: string
format: uuid
description:
type: string
actionType:
type: string
enum: [CORRECTIVE, PREVENTIVE]
status:
type: string
enum: [PENDING, IN_PROGRESS, COMPLETED, VERIFIED]
assignedTo:
type: string
format: uuid
dueDate:
type: string
format: date-time
completedAt:
type: string
format: date-time
verifiedBy:
type: string
format: uuid
verifiedAt:
type: string
format: date-time

Deviation:
type: object
required:
- id
- title
- severity
- status
- reportedBy
- reportedAt
properties:
id:
type: string
format: uuid
deviationNumber:
type: string
description: Auto-generated deviation number (e.g., DEV-2026-0123)
title:
type: string
description:
type: string
severity:
type: string
enum: [MINOR, MAJOR, CRITICAL]
status:
type: string
enum: [REPORTED, UNDER_INVESTIGATION, RESOLVED, CLOSED]
reportedBy:
type: string
format: uuid
reportedAt:
type: string
format: date-time
investigationLeadId:
type: string
format: uuid
rootCauseAnalysis:
type: string
impactAssessment:
type: string
closedAt:
type: string
format: date-time
relatedCapas:
type: array
items:
type: string
format: uuid

CreateDeviationDto:
type: object
required:
- title
- description
- severity
properties:
title:
type: string
description:
type: string
severity:
type: string
enum: [MINOR, MAJOR, CRITICAL]
relatedDocumentId:
type: string
format: uuid
relatedProcessId:
type: string
format: uuid

User:
type: object
required:
- id
- email
- firstName
- lastName
- role
- isActive
properties:
id:
type: string
format: uuid
email:
type: string
format: email
firstName:
type: string
lastName:
type: string
role:
type: string
enum: [ADMIN, QUALITY_MANAGER, OPERATOR, REVIEWER, APPROVER]
departmentId:
type: string
format: uuid
isActive:
type: boolean
createdAt:
type: string
format: date-time
lastLoginAt:
type: string
format: date-time
trainingStatus:
type: object
properties:
currentTrainings:
type: integer
completedTrainings:
type: integer
overdueTrainings:
type: integer

CreateUserDto:
type: object
required:
- email
- firstName
- lastName
- role
- departmentId
properties:
email:
type: string
format: email
firstName:
type: string
lastName:
type: string
role:
type: string
enum: [ADMIN, QUALITY_MANAGER, OPERATOR, REVIEWER, APPROVER]
departmentId:
type: string
format: uuid

AuditEntry:
type: object
required:
- id
- entityType
- entityId
- action
- userId
- timestamp
properties:
id:
type: string
format: uuid
entityType:
type: string
enum: [DOCUMENT, CAPA, DEVIATION, USER, TRAINING, SIGNATURE]
entityId:
type: string
format: uuid
action:
type: string
enum: [CREATE, UPDATE, DELETE, APPROVE, REJECT, SIGN, ESCALATE, VIEW]
userId:
type: string
format: uuid
userName:
type: string
timestamp:
type: string
format: date-time
ipAddress:
type: string
userAgent:
type: string
changes:
type: object
description: JSON diff of changes
previousHash:
type: string
description: Hash of previous audit entry for chain verification
currentHash:
type: string
description: SHA-256 hash of this entry
metadata:
type: object
additionalProperties: true

PaginationMeta:
type: object
properties:
page:
type: integer
limit:
type: integer
total:
type: integer
totalPages:
type: integer

responses:
Unauthorized:
description: Unauthorized - Invalid or missing authentication token
content:
application/json:
schema:
type: object
properties:
statusCode:
type: integer
example: 401
message:
type: string
example: Unauthorized
error:
type: string
example: Invalid token

Forbidden:
description: Forbidden - Insufficient permissions
content:
application/json:
schema:
type: object
properties:
statusCode:
type: integer
example: 403
message:
type: string
example: Forbidden resource
error:
type: string
example: Insufficient permissions

NotFound:
description: Resource not found
content:
application/json:
schema:
type: object
properties:
statusCode:
type: integer
example: 404
message:
type: string
example: Resource not found
error:
type: string
example: Not Found

BadRequest:
description: Bad request - Validation error
content:
application/json:
schema:
type: object
properties:
statusCode:
type: integer
example: 400
message:
type: array
items:
type: string
example: ["title should not be empty", "type must be a valid enum value"]
error:
type: string
example: Bad Request

RateLimitExceeded:
description: Rate limit exceeded
content:
application/json:
schema:
type: object
properties:
statusCode:
type: integer
example: 429
message:
type: string
example: Too many requests
retryAfter:
type: integer
description: Seconds until rate limit resets

securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
oauth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.bio-qms.com/oauth/authorize
tokenUrl: https://auth.bio-qms.com/oauth/token
scopes:
read:documents: Read documents
write:documents: Create and update documents
delete:documents: Delete documents
read:capas: Read CAPAs
write:capas: Create and update CAPAs
read:deviations: Read deviations
write:deviations: Create and update deviations
read:users: Read user information
write:users: Manage users
read:audit: Read audit trail
admin: Full administrative access

F.2.2: API Authentication Guide

Authentication Methods

BIO-QMS API supports three authentication methods:

MethodUse CaseToken Lifetime
OAuth2 Authorization CodeWeb applications, user-delegated accessAccess: 1 hour, Refresh: 30 days
JWT Bearer TokenService-to-service, API clients24 hours
API KeysLong-lived integrations, automationNo expiration (rotatable)

OAuth2 Authorization Flow

Step-by-Step OAuth2 Implementation

Step 1: Register Your Application

Register your application at the BIO-QMS Developer Portal to obtain:

  • Client ID: bio_qms_client_abc123xyz
  • Client Secret: secret_def456uvw (keep secure!)
  • Redirect URI: https://yourapp.com/oauth/callback

Step 2: Authorization Request

GET https://auth.bio-qms.com/oauth/authorize?
response_type=code&
client_id=bio_qms_client_abc123xyz&
redirect_uri=https://yourapp.com/oauth/callback&
scope=read:documents write:documents read:capas write:capas&
state=random_state_string

Parameters:

  • response_type: Always code for authorization code flow
  • client_id: Your application client ID
  • redirect_uri: Must match registered redirect URI
  • scope: Space-separated list of requested permissions
  • state: Random string to prevent CSRF attacks

Step 3: Exchange Authorization Code for Tokens

After user authorization, BIO-QMS redirects to:

https://yourapp.com/oauth/callback?code=AUTH_CODE_HERE&state=random_state_string

Exchange the code for tokens:

curl -X POST https://auth.bio-qms.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE_HERE" \
-d "client_id=bio_qms_client_abc123xyz" \
-d "client_secret=secret_def456uvw" \
-d "redirect_uri=https://yourapp.com/oauth/callback"

Response:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"scope": "read:documents write:documents read:capas write:capas"
}

Step 4: Refresh Access Token

When the access token expires:

curl -X POST https://auth.bio-qms.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=REFRESH_TOKEN_HERE" \
-d "client_id=bio_qms_client_abc123xyz" \
-d "client_secret=secret_def456uvw"

JWT Token Authentication

For service-to-service authentication, obtain a JWT token directly:

curl -X POST https://auth.bio-qms.com/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "api-user@yourcompany.com",
"password": "your_secure_password"
}'

Response:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAiLCJlbWFpbCI6ImFwaS11c2VyQHlvdXJjb21wYW55LmNvbSIsInJvbGUiOiJBRE1JTiIsImlhdCI6MTcwODUzMDAwMCwiZXhwIjoxNzA4NjE2NDAwfQ.signature",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "api-user@yourcompany.com",
"firstName": "API",
"lastName": "User",
"role": "ADMIN"
}
}

API Key Management

For long-lived integrations, use API keys:

Create an API Key

curl -X POST https://api.bio-qms.com/v1/api-keys \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "ERP Integration Key",
"scopes": ["read:documents", "write:documents", "read:audit"],
"expiresAt": null
}'

Response:

{
"id": "apk_1234567890abcdef",
"key": "bio_qms_sk_live_abcdefghijklmnopqrstuvwxyz123456",
"name": "ERP Integration Key",
"scopes": ["read:documents", "write:documents", "read:audit"],
"createdAt": "2026-02-17T10:00:00Z",
"lastUsedAt": null,
"expiresAt": null
}

IMPORTANT: Store the key value securely - it will only be shown once.

Use API Key

curl -X GET https://api.bio-qms.com/v1/documents \
-H "Authorization: Bearer bio_qms_sk_live_abcdefghijklmnopqrstuvwxyz123456"

Rotate API Key

curl -X POST https://api.bio-qms.com/v1/api-keys/apk_1234567890abcdef/rotate \
-H "Authorization: Bearer YOUR_JWT_TOKEN"

Response includes new key value.

Revoke API Key

curl -X DELETE https://api.bio-qms.com/v1/api-keys/apk_1234567890abcdef \
-H "Authorization: Bearer YOUR_JWT_TOKEN"

Code Examples

cURL

# List documents with Bearer token
curl -X GET "https://api.bio-qms.com/v1/documents?page=1&limit=20&status=APPROVED" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Accept: application/json"

# Create a new CAPA
curl -X POST "https://api.bio-qms.com/v1/capas" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Investigate Equipment Failure",
"description": "Centrifuge 3 failed during batch processing",
"priority": "HIGH",
"assignedTo": "550e8400-e29b-41d4-a716-446655440000",
"dueDate": "2026-03-01T00:00:00Z",
"rootCause": "Worn bearing in motor assembly"
}'

# Report a deviation
curl -X POST "https://api.bio-qms.com/v1/deviations" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Temperature Excursion in Storage",
"description": "Refrigerator 2 exceeded 8°C for 15 minutes",
"severity": "MAJOR"
}'

Python

import requests
from typing import Dict, List, Optional
from datetime import datetime, timedelta

class BioQMSClient:
"""Python client for BIO-QMS API"""

def __init__(self, base_url: str = "https://api.bio-qms.com/v1"):
self.base_url = base_url
self.access_token: Optional[str] = None
self.token_expiry: Optional[datetime] = None

def login(self, email: str, password: str) -> Dict:
"""Authenticate and obtain JWT token"""
response = requests.post(
"https://auth.bio-qms.com/auth/login",
json={"email": email, "password": password}
)
response.raise_for_status()
data = response.json()

self.access_token = data["access_token"]
# JWT typically expires in 1 hour
self.token_expiry = datetime.now() + timedelta(hours=1)

return data

def _get_headers(self) -> Dict[str, str]:
"""Get request headers with authorization"""
if not self.access_token:
raise ValueError("Not authenticated. Call login() first.")

return {
"Authorization": f"Bearer {self.access_token}",
"Content-Type": "application/json"
}

def list_documents(
self,
page: int = 1,
limit: int = 20,
status: Optional[str] = None,
doc_type: Optional[str] = None
) -> Dict:
"""List documents with optional filtering"""
params = {"page": page, "limit": limit}
if status:
params["status"] = status
if doc_type:
params["type"] = doc_type

response = requests.get(
f"{self.base_url}/documents",
headers=self._get_headers(),
params=params
)
response.raise_for_status()
return response.json()

def get_document(self, document_id: str) -> Dict:
"""Get a specific document by ID"""
response = requests.get(
f"{self.base_url}/documents/{document_id}",
headers=self._get_headers()
)
response.raise_for_status()
return response.json()

def create_document(self, document_data: Dict) -> Dict:
"""Create a new document"""
response = requests.post(
f"{self.base_url}/documents",
headers=self._get_headers(),
json=document_data
)
response.raise_for_status()
return response.json()

def create_capa(self, capa_data: Dict) -> Dict:
"""Create a new CAPA"""
response = requests.post(
f"{self.base_url}/capas",
headers=self._get_headers(),
json=capa_data
)
response.raise_for_status()
return response.json()

def list_capas(
self,
status: Optional[str] = None,
priority: Optional[str] = None,
assigned_to: Optional[str] = None
) -> Dict:
"""List CAPAs with filtering"""
params = {}
if status:
params["status"] = status
if priority:
params["priority"] = priority
if assigned_to:
params["assignedTo"] = assigned_to

response = requests.get(
f"{self.base_url}/capas",
headers=self._get_headers(),
params=params
)
response.raise_for_status()
return response.json()

def report_deviation(self, deviation_data: Dict) -> Dict:
"""Report a new deviation"""
response = requests.post(
f"{self.base_url}/deviations",
headers=self._get_headers(),
json=deviation_data
)
response.raise_for_status()
return response.json()

def escalate_deviation(self, deviation_id: str, reason: str, escalated_to: str) -> Dict:
"""Escalate a deviation"""
response = requests.post(
f"{self.base_url}/deviations/{deviation_id}/escalate",
headers=self._get_headers(),
json={
"reason": reason,
"escalatedTo": escalated_to,
"newSeverity": "CRITICAL"
}
)
response.raise_for_status()
return response.json()

def query_audit_trail(
self,
date_from: datetime,
date_to: datetime,
entity_type: Optional[str] = None,
entity_id: Optional[str] = None,
page: int = 1,
limit: int = 50
) -> Dict:
"""Query audit trail"""
params = {
"dateFrom": date_from.isoformat(),
"dateTo": date_to.isoformat(),
"page": page,
"limit": limit
}
if entity_type:
params["entityType"] = entity_type
if entity_id:
params["entityId"] = entity_id

response = requests.get(
f"{self.base_url}/audit-trail",
headers=self._get_headers(),
params=params
)
response.raise_for_status()
return response.json()


# Usage Example
if __name__ == "__main__":
# Initialize client
client = BioQMSClient()

# Login
client.login(
email="api-user@yourcompany.com",
password="your_secure_password"
)

# List approved SOPs
documents = client.list_documents(
status="APPROVED",
doc_type="SOP",
limit=50
)
print(f"Found {documents['meta']['total']} approved SOPs")

# Create a new CAPA
capa = client.create_capa({
"title": "Investigate Equipment Failure",
"description": "Centrifuge 3 failed during batch processing",
"priority": "HIGH",
"assignedTo": "550e8400-e29b-41d4-a716-446655440000",
"dueDate": "2026-03-01T00:00:00Z",
"rootCause": "Worn bearing in motor assembly"
})
print(f"Created CAPA: {capa['capaNumber']}")

# Report a deviation
deviation = client.report_deviation({
"title": "Temperature Excursion in Storage",
"description": "Refrigerator 2 exceeded 8°C for 15 minutes",
"severity": "MAJOR"
})
print(f"Reported deviation: {deviation['deviationNumber']}")

# Query audit trail for last 30 days
from datetime import datetime, timedelta
audit = client.query_audit_trail(
date_from=datetime.now() - timedelta(days=30),
date_to=datetime.now(),
entity_type="DOCUMENT"
)
print(f"Audit trail entries: {audit['meta']['total']}")

JavaScript/TypeScript

// TypeScript SDK for BIO-QMS API
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

interface AuthResponse {
access_token: string;
user: {
id: string;
email: string;
firstName: string;
lastName: string;
role: string;
};
}

interface PaginatedResponse<T> {
data: T[];
meta: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}

interface Document {
id: string;
title: string;
type: 'SOP' | 'PROTOCOL' | 'REPORT' | 'FORM' | 'SPECIFICATION';
status: 'DRAFT' | 'REVIEW' | 'APPROVED' | 'EFFECTIVE' | 'OBSOLETE';
version: number;
documentNumber: string;
createdAt: string;
updatedAt: string;
}

interface Capa {
id: string;
capaNumber: string;
title: string;
description: string;
status: 'OPEN' | 'IN_PROGRESS' | 'PENDING_VERIFICATION' | 'VERIFIED' | 'CLOSED';
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
assignedTo: string;
dueDate: string;
createdAt: string;
}

interface Deviation {
id: string;
deviationNumber: string;
title: string;
description: string;
severity: 'MINOR' | 'MAJOR' | 'CRITICAL';
status: 'REPORTED' | 'UNDER_INVESTIGATION' | 'RESOLVED' | 'CLOSED';
reportedBy: string;
reportedAt: string;
}

export class BioQMSClient {
private client: AxiosInstance;
private accessToken?: string;

constructor(baseURL: string = 'https://api.bio-qms.com/v1') {
this.client = axios.create({
baseURL,
headers: {
'Content-Type': 'application/json',
},
});

// Add request interceptor to include auth token
this.client.interceptors.request.use((config) => {
if (this.accessToken) {
config.headers.Authorization = `Bearer ${this.accessToken}`;
}
return config;
});
}

async login(email: string, password: string): Promise<AuthResponse> {
const response = await axios.post<AuthResponse>(
'https://auth.bio-qms.com/auth/login',
{ email, password }
);
this.accessToken = response.data.access_token;
return response.data;
}

async listDocuments(params?: {
page?: number;
limit?: number;
status?: string;
type?: string;
}): Promise<PaginatedResponse<Document>> {
const response = await this.client.get<PaginatedResponse<Document>>(
'/documents',
{ params }
);
return response.data;
}

async getDocument(id: string): Promise<Document> {
const response = await this.client.get<Document>(`/documents/${id}`);
return response.data;
}

async createDocument(data: {
title: string;
type: string;
departmentId: string;
description?: string;
}): Promise<Document> {
const response = await this.client.post<Document>('/documents', data);
return response.data;
}

async listCapas(params?: {
status?: string;
priority?: string;
assignedTo?: string;
}): Promise<PaginatedResponse<Capa>> {
const response = await this.client.get<PaginatedResponse<Capa>>(
'/capas',
{ params }
);
return response.data;
}

async createCapa(data: {
title: string;
description: string;
priority: string;
assignedTo: string;
dueDate: string;
rootCause?: string;
}): Promise<Capa> {
const response = await this.client.post<Capa>('/capas', data);
return response.data;
}

async reportDeviation(data: {
title: string;
description: string;
severity: string;
}): Promise<Deviation> {
const response = await this.client.post<Deviation>('/deviations', data);
return response.data;
}

async escalateDeviation(
id: string,
data: {
reason: string;
escalatedTo: string;
newSeverity?: string;
}
): Promise<Deviation> {
const response = await this.client.post<Deviation>(
`/deviations/${id}/escalate`,
data
);
return response.data;
}

async queryAuditTrail(params: {
dateFrom: string;
dateTo: string;
entityType?: string;
entityId?: string;
page?: number;
limit?: number;
}): Promise<PaginatedResponse<any>> {
const response = await this.client.get('/audit-trail', { params });
return response.data;
}
}

// Usage Example
async function example() {
const client = new BioQMSClient();

// Login
await client.login('api-user@yourcompany.com', 'your_secure_password');

// List approved documents
const documents = await client.listDocuments({
status: 'APPROVED',
type: 'SOP',
limit: 50,
});
console.log(`Found ${documents.meta.total} approved SOPs`);

// Create a CAPA
const capa = await client.createCapa({
title: 'Investigate Equipment Failure',
description: 'Centrifuge 3 failed during batch processing',
priority: 'HIGH',
assignedTo: '550e8400-e29b-41d4-a716-446655440000',
dueDate: '2026-03-01T00:00:00Z',
rootCause: 'Worn bearing in motor assembly',
});
console.log(`Created CAPA: ${capa.capaNumber}`);

// Report a deviation
const deviation = await client.reportDeviation({
title: 'Temperature Excursion in Storage',
description: 'Refrigerator 2 exceeded 8°C for 15 minutes',
severity: 'MAJOR',
});
console.log(`Reported deviation: ${deviation.deviationNumber}`);
}

Auto-Generated Client SDKs

BIO-QMS provides official SDKs generated from the OpenAPI specification:

TypeScript SDK

npm install @bio-qms/api-client
import { BioQMSClient } from '@bio-qms/api-client';

const client = new BioQMSClient({
apiKey: 'bio_qms_sk_live_abcdefghijklmnopqrstuvwxyz123456',
});

const documents = await client.documents.list({ status: 'APPROVED' });

Python SDK

pip install bio-qms-python
from bio_qms import BioQMSClient

client = BioQMSClient(api_key='bio_qms_sk_live_abcdefghijklmnopqrstuvwxyz123456')

documents = client.documents.list(status='APPROVED')

SDK Features:

  • Full type safety (TypeScript) and type hints (Python)
  • Auto-retry with exponential backoff
  • Built-in pagination helpers
  • Webhook signature verification
  • Error handling with detailed error types

F.2.3: Webhook Integration Guide

Overview

BIO-QMS webhooks allow your application to receive real-time notifications when events occur in the system. Webhooks are HTTP POST requests sent to a URL you configure.

Key Features:

  • Real-time event notifications
  • At-least-once delivery guarantee
  • Automatic retry with exponential backoff
  • HMAC signature verification for security
  • Event filtering by type
  • Test endpoints for development

Webhook Events

Event TypeDescriptionPayload
document.createdNew document createdFull document object
document.approvedDocument approvedDocument object with approval metadata
document.rejectedDocument rejectedDocument object with rejection reason
document.obsoletedDocument marked obsoleteDocument object
capa.createdNew CAPA createdFull CAPA object
capa.assignedCAPA assigned to userCAPA object with assignee
capa.completedCAPA marked completeCAPA object
capa.verifiedCAPA verification completeCAPA object with verification
deviation.createdNew deviation reportedFull deviation object
deviation.escalatedDeviation escalatedDeviation object with escalation details
deviation.resolvedDeviation resolvedDeviation object with resolution
training.assignedTraining assigned to userTraining assignment object
training.completedTraining completedTraining completion with signature
training.overdueTraining becomes overdueTraining object
signature.appliedElectronic signature appliedSignature with CFR Part 11 metadata
audit.alertAudit trail integrity alertAlert details

Webhook Configuration

Create a Webhook Endpoint

curl -X POST https://api.bio-qms.com/v1/webhooks \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/bio-qms",
"events": [
"document.approved",
"capa.created",
"deviation.escalated",
"training.completed"
],
"description": "Production webhook for ERP integration",
"secret": "whsec_your_webhook_signing_secret_here"
}'

Response:

{
"id": "wh_1234567890abcdef",
"url": "https://your-app.com/webhooks/bio-qms",
"events": [
"document.approved",
"capa.created",
"deviation.escalated",
"training.completed"
],
"description": "Production webhook for ERP integration",
"secret": "whsec_your_webhook_signing_secret_here",
"status": "active",
"createdAt": "2026-02-17T10:00:00Z"
}

IMPORTANT: Store the secret securely - it's used to verify webhook signatures.

List Webhook Endpoints

curl -X GET https://api.bio-qms.com/v1/webhooks \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Update Webhook Endpoint

curl -X PATCH https://api.bio-qms.com/v1/webhooks/wh_1234567890abcdef \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"events": [
"document.approved",
"capa.created",
"deviation.escalated",
"training.completed",
"signature.applied"
]
}'

Delete Webhook Endpoint

curl -X DELETE https://api.bio-qms.com/v1/webhooks/wh_1234567890abcdef \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Webhook Payload Format

All webhook payloads follow this structure:

{
"id": "evt_1234567890abcdef",
"type": "document.approved",
"createdAt": "2026-02-17T10:30:00Z",
"data": {
"object": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "SOP-001: Manufacturing Process Validation",
"type": "SOP",
"status": "APPROVED",
"version": 2,
"approvedAt": "2026-02-17T10:30:00Z",
"approvedBy": "660e8400-e29b-41d4-a716-446655440001"
},
"previousAttributes": {
"status": "REVIEW"
}
},
"metadata": {
"userId": "660e8400-e29b-41d4-a716-446655440001",
"ipAddress": "192.168.1.100",
"userAgent": "Mozilla/5.0..."
}
}

Example Payloads

document.approved

{
"id": "evt_doc_approved_001",
"type": "document.approved",
"createdAt": "2026-02-17T10:30:00Z",
"data": {
"object": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"documentNumber": "DOC-2026-0042",
"title": "SOP-001: Manufacturing Process Validation",
"type": "SOP",
"status": "APPROVED",
"version": 2,
"approvedAt": "2026-02-17T10:30:00Z",
"approvedBy": "660e8400-e29b-41d4-a716-446655440001",
"approverName": "Jane Smith",
"effectiveDate": "2026-03-01T00:00:00Z"
},
"previousAttributes": {
"status": "REVIEW"
}
}
}

capa.created

{
"id": "evt_capa_created_001",
"type": "capa.created",
"createdAt": "2026-02-17T11:00:00Z",
"data": {
"object": {
"id": "770e8400-e29b-41d4-a716-446655440002",
"capaNumber": "CAPA-2026-0042",
"title": "Investigate Equipment Failure",
"description": "Centrifuge 3 failed during batch processing",
"status": "OPEN",
"priority": "HIGH",
"assignedTo": "550e8400-e29b-41d4-a716-446655440000",
"assigneeName": "John Doe",
"dueDate": "2026-03-01T00:00:00Z",
"createdBy": "660e8400-e29b-41d4-a716-446655440001",
"createdAt": "2026-02-17T11:00:00Z",
"relatedDeviationId": "880e8400-e29b-41d4-a716-446655440003"
}
}
}

deviation.escalated

{
"id": "evt_dev_escalated_001",
"type": "deviation.escalated",
"createdAt": "2026-02-17T12:00:00Z",
"data": {
"object": {
"id": "880e8400-e29b-41d4-a716-446655440003",
"deviationNumber": "DEV-2026-0123",
"title": "Temperature Excursion in Storage",
"description": "Refrigerator 2 exceeded 8°C for 15 minutes",
"severity": "CRITICAL",
"status": "UNDER_INVESTIGATION",
"escalatedAt": "2026-02-17T12:00:00Z",
"escalatedBy": "660e8400-e29b-41d4-a716-446655440001",
"escalatedTo": "990e8400-e29b-41d4-a716-446655440004",
"escalationReason": "Potential product impact requires immediate QA review"
},
"previousAttributes": {
"severity": "MAJOR"
}
}
}

training.completed

{
"id": "evt_training_completed_001",
"type": "training.completed",
"createdAt": "2026-02-17T13:00:00Z",
"data": {
"object": {
"id": "aa0e8400-e29b-41d4-a716-446655440005",
"trainingId": "bb0e8400-e29b-41d4-a716-446655440006",
"trainingTitle": "GMP Basics Module 1",
"userId": "550e8400-e29b-41d4-a716-446655440000",
"userName": "John Doe",
"completedAt": "2026-02-17T13:00:00Z",
"score": 95,
"passingScore": 80,
"certificateId": "CERT-2026-0100",
"signatureApplied": true,
"signatureMetadata": {
"signedBy": "550e8400-e29b-41d4-a716-446655440000",
"signedAt": "2026-02-17T13:00:00Z",
"ipAddress": "192.168.1.100",
"meaning": "I certify completion of this training",
"cfrPart11Compliant": true
}
}
}
}

signature.applied

{
"id": "evt_signature_applied_001",
"type": "signature.applied",
"createdAt": "2026-02-17T14:00:00Z",
"data": {
"object": {
"id": "cc0e8400-e29b-41d4-a716-446655440007",
"entityType": "DOCUMENT",
"entityId": "550e8400-e29b-41d4-a716-446655440000",
"signedBy": "660e8400-e29b-41d4-a716-446655440001",
"signerName": "Jane Smith",
"signedAt": "2026-02-17T14:00:00Z",
"meaning": "I approve this document",
"reason": "Annual SOP review completed",
"ipAddress": "192.168.1.101",
"userAgent": "Mozilla/5.0...",
"cfrPart11Metadata": {
"userIdentity": "jane.smith@company.com",
"passwordVerified": true,
"twoFactorVerified": true,
"timestamp": "2026-02-17T14:00:00.123Z",
"cryptographicHash": "sha256:abcdef123456...",
"auditTrailEntry": "dd0e8400-e29b-41d4-a716-446655440008"
}
}
}
}

Webhook Security (HMAC Signatures)

All webhook requests include an HMAC signature in the X-BioQMS-Signature header for verification.

Signature Calculation:

HMAC-SHA256(webhook_secret, request_body)

Verify Webhook Signature (Python)

import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

WEBHOOK_SECRET = "whsec_your_webhook_signing_secret_here"

def verify_webhook_signature(payload: bytes, signature: str) -> bool:
"""Verify webhook HMAC signature"""
expected_signature = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()

return hmac.compare_digest(signature, expected_signature)

@app.route('/webhooks/bio-qms', methods=['POST'])
def handle_webhook():
# Get signature from header
signature = request.headers.get('X-BioQMS-Signature')
if not signature:
return jsonify({'error': 'Missing signature'}), 401

# Verify signature
payload = request.get_data()
if not verify_webhook_signature(payload, signature):
return jsonify({'error': 'Invalid signature'}), 401

# Parse event
event = request.json
event_type = event['type']

# Handle different event types
if event_type == 'document.approved':
handle_document_approved(event['data']['object'])
elif event_type == 'capa.created':
handle_capa_created(event['data']['object'])
elif event_type == 'deviation.escalated':
handle_deviation_escalated(event['data']['object'])
elif event_type == 'training.completed':
handle_training_completed(event['data']['object'])

return jsonify({'received': True}), 200

def handle_document_approved(document):
print(f"Document approved: {document['documentNumber']}")
# Your business logic here

def handle_capa_created(capa):
print(f"CAPA created: {capa['capaNumber']}")
# Your business logic here

def handle_deviation_escalated(deviation):
print(f"Deviation escalated: {deviation['deviationNumber']}")
# Send alert, create ticket, etc.

def handle_training_completed(training):
print(f"Training completed: {training['userName']} - {training['trainingTitle']}")
# Update HR system, send certificate, etc.

Verify Webhook Signature (Node.js/TypeScript)

import express from 'express';
import crypto from 'crypto';

const app = express();
const WEBHOOK_SECRET = 'whsec_your_webhook_signing_secret_here';

// Use express.raw() for webhook endpoints to preserve raw body
app.use('/webhooks/bio-qms', express.raw({ type: 'application/json' }));

function verifyWebhookSignature(payload: Buffer, signature: string): boolean {
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');

return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}

app.post('/webhooks/bio-qms', (req, res) => {
const signature = req.headers['x-bioqms-signature'] as string;

if (!signature) {
return res.status(401).json({ error: 'Missing signature' });
}

if (!verifyWebhookSignature(req.body, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}

const event = JSON.parse(req.body.toString());

switch (event.type) {
case 'document.approved':
handleDocumentApproved(event.data.object);
break;
case 'capa.created':
handleCapaCreated(event.data.object);
break;
case 'deviation.escalated':
handleDeviationEscalated(event.data.object);
break;
case 'training.completed':
handleTrainingCompleted(event.data.object);
break;
}

res.json({ received: true });
});

function handleDocumentApproved(document: any) {
console.log(`Document approved: ${document.documentNumber}`);
}

function handleCapaCreated(capa: any) {
console.log(`CAPA created: ${capa.capaNumber}`);
}

function handleDeviationEscalated(deviation: any) {
console.log(`Deviation escalated: ${deviation.deviationNumber}`);
}

function handleTrainingCompleted(training: any) {
console.log(`Training completed: ${training.userName} - ${training.trainingTitle}`);
}

app.listen(3000, () => console.log('Webhook server listening on port 3000'));

Delivery Guarantees and Retries

At-Least-Once Delivery:

  • BIO-QMS guarantees webhook delivery at least once
  • Your endpoint should be idempotent (handle duplicate events gracefully)
  • Use the event.id field to deduplicate events

Retry Logic:

  • Failed webhooks retry with exponential backoff
  • Retry schedule: 1s, 5s, 30s, 2m, 10m, 1h, 6h, 24h
  • Webhooks disabled after 7 consecutive days of failures
  • Re-enable via API or dashboard

Successful Response:

  • Return HTTP 2xx status code within 30 seconds
  • Response body ignored

Failed Response:

  • HTTP 4xx/5xx triggers retry
  • Timeout after 30 seconds triggers retry

Test Webhook Endpoint

BIO-QMS provides a test endpoint to send sample events:

curl -X POST https://api.bio-qms.com/v1/webhooks/wh_1234567890abcdef/test \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"eventType": "document.approved"
}'

Response:

{
"success": true,
"deliveryStatus": "delivered",
"statusCode": 200,
"responseTime": 234,
"eventId": "evt_test_1234567890"
}

Webhook Logs

View webhook delivery logs:

curl -X GET "https://api.bio-qms.com/v1/webhooks/wh_1234567890abcdef/logs?limit=50" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response:

{
"data": [
{
"id": "log_1234567890",
"eventId": "evt_doc_approved_001",
"eventType": "document.approved",
"attemptNumber": 1,
"statusCode": 200,
"responseTime": 234,
"deliveredAt": "2026-02-17T10:30:05Z",
"success": true
},
{
"id": "log_0987654321",
"eventId": "evt_capa_created_002",
"eventType": "capa.created",
"attemptNumber": 2,
"statusCode": 503,
"responseTime": 30000,
"deliveredAt": "2026-02-17T09:15:35Z",
"success": false,
"errorMessage": "Timeout after 30 seconds"
}
],
"meta": {
"page": 1,
"limit": 50,
"total": 2,
"totalPages": 1
}
}

F.2.4: Integration Cookbook with Code Examples

Integration Cookbook Overview

This section provides complete, production-ready code examples for common integration scenarios:

  1. ERP Sync: Sync approved documents to ERP system
  2. LIMS Integration: Create deviations from LIMS out-of-spec results
  3. Document Import: Bulk import existing documents
  4. Custom Workflows: Build custom approval workflows

Recipe 1: ERP Sync

Scenario: Automatically sync approved SOPs to your ERP system for manufacturing execution.

Architecture

Python Implementation with Celery

# webhook_handler.py - Flask webhook receiver
from flask import Flask, request, jsonify
import hmac
import hashlib
from celery_app import sync_document_to_erp

app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_webhook_signing_secret_here"

def verify_signature(payload: bytes, signature: str) -> bool:
expected = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)

@app.route('/webhooks/bio-qms', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-BioQMS-Signature')
if not signature or not verify_signature(request.get_data(), signature):
return jsonify({'error': 'Invalid signature'}), 401

event = request.json

# Only sync approved SOPs
if event['type'] == 'document.approved' and event['data']['object']['type'] == 'SOP':
document_id = event['data']['object']['id']
# Enqueue async job
sync_document_to_erp.delay(document_id)

return jsonify({'received': True}), 200

if __name__ == '__main__':
app.run(port=5000)
# celery_app.py - Celery worker for ERP sync
from celery import Celery
import requests
from typing import Dict

app = Celery('erp_sync', broker='redis://localhost:6379/0')

BIO_QMS_API_KEY = "bio_qms_sk_live_abcdefghijklmnopqrstuvwxyz123456"
ERP_API_URL = "https://erp.yourcompany.com/api"
ERP_API_KEY = "erp_api_key_here"

@app.task(bind=True, max_retries=3)
def sync_document_to_erp(self, document_id: str):
"""Sync BIO-QMS document to ERP system"""
try:
# 1. Fetch full document details from BIO-QMS
response = requests.get(
f"https://api.bio-qms.com/v1/documents/{document_id}",
headers={"Authorization": f"Bearer {BIO_QMS_API_KEY}"}
)
response.raise_for_status()
document = response.json()

# 2. Transform to ERP format
erp_sop = {
"external_id": document['id'],
"document_number": document['documentNumber'],
"title": document['title'],
"version": document['version'],
"effective_date": document['effectiveDate'],
"approved_by": document.get('approverName', ''),
"status": "active",
"content_url": f"https://app.bio-qms.com/documents/{document['id']}"
}

# 3. Check if SOP already exists in ERP
existing = requests.get(
f"{ERP_API_URL}/sops?external_id={document['id']}",
headers={"Authorization": f"Bearer {ERP_API_KEY}"}
)

if existing.status_code == 200 and existing.json()['data']:
# Update existing
erp_id = existing.json()['data'][0]['id']
response = requests.put(
f"{ERP_API_URL}/sops/{erp_id}",
headers={"Authorization": f"Bearer {ERP_API_KEY}"},
json=erp_sop
)
else:
# Create new
response = requests.post(
f"{ERP_API_URL}/sops",
headers={"Authorization": f"Bearer {ERP_API_KEY}"},
json=erp_sop
)

response.raise_for_status()
print(f"Successfully synced document {document['documentNumber']} to ERP")

except requests.exceptions.RequestException as exc:
# Retry on network errors
raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries))
# Start the Celery worker
celery -A celery_app worker --loglevel=info

TypeScript Implementation with Bull Queue

// webhook-handler.ts - Express webhook receiver
import express from 'express';
import crypto from 'crypto';
import { syncQueue } from './queue';

const app = express();
const WEBHOOK_SECRET = 'whsec_your_webhook_signing_secret_here';

app.use('/webhooks/bio-qms', express.raw({ type: 'application/json' }));

function verifySignature(payload: Buffer, signature: string): boolean {
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

app.post('/webhooks/bio-qms', async (req, res) => {
const signature = req.headers['x-bioqms-signature'] as string;

if (!signature || !verifySignature(req.body, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}

const event = JSON.parse(req.body.toString());

if (event.type === 'document.approved' && event.data.object.type === 'SOP') {
await syncQueue.add('sync-to-erp', {
documentId: event.data.object.id,
});
}

res.json({ received: true });
});

app.listen(3000);
// queue.ts - Bull queue configuration
import Queue from 'bull';

export const syncQueue = new Queue('erp-sync', {
redis: { host: 'localhost', port: 6379 },
});
// worker.ts - Bull worker for ERP sync
import { syncQueue } from './queue';
import axios from 'axios';

const BIO_QMS_API_KEY = 'bio_qms_sk_live_abcdefghijklmnopqrstuvwxyz123456';
const ERP_API_URL = 'https://erp.yourcompany.com/api';
const ERP_API_KEY = 'erp_api_key_here';

syncQueue.process('sync-to-erp', async (job) => {
const { documentId } = job.data;

// 1. Fetch document from BIO-QMS
const docResponse = await axios.get(
`https://api.bio-qms.com/v1/documents/${documentId}`,
{ headers: { Authorization: `Bearer ${BIO_QMS_API_KEY}` } }
);
const document = docResponse.data;

// 2. Transform to ERP format
const erpSop = {
external_id: document.id,
document_number: document.documentNumber,
title: document.title,
version: document.version,
effective_date: document.effectiveDate,
approved_by: document.approverName || '',
status: 'active',
content_url: `https://app.bio-qms.com/documents/${document.id}`,
};

// 3. Check if exists
const existingResponse = await axios.get(
`${ERP_API_URL}/sops?external_id=${document.id}`,
{ headers: { Authorization: `Bearer ${ERP_API_KEY}` } }
);

if (existingResponse.data.data.length > 0) {
// Update
const erpId = existingResponse.data.data[0].id;
await axios.put(`${ERP_API_URL}/sops/${erpId}`, erpSop, {
headers: { Authorization: `Bearer ${ERP_API_KEY}` },
});
} else {
// Create
await axios.post(`${ERP_API_URL}/sops`, erpSop, {
headers: { Authorization: `Bearer ${ERP_API_KEY}` },
});
}

console.log(`Synced ${document.documentNumber} to ERP`);
});

console.log('ERP sync worker started');

Recipe 2: LIMS Integration

Scenario: Automatically create deviations in BIO-QMS when LIMS reports out-of-spec test results.

Python Implementation

# lims_integration.py - Poll LIMS for out-of-spec results
import requests
import time
from datetime import datetime, timedelta
from typing import List, Dict

BIO_QMS_API_KEY = "bio_qms_sk_live_abcdefghijklmnopqrstuvwxyz123456"
LIMS_API_URL = "https://lims.yourcompany.com/api"
LIMS_API_KEY = "lims_api_key_here"

def fetch_out_of_spec_results(since: datetime) -> List[Dict]:
"""Fetch out-of-spec test results from LIMS"""
response = requests.get(
f"{LIMS_API_URL}/test-results",
headers={"Authorization": f"Bearer {LIMS_API_KEY}"},
params={
"status": "out_of_spec",
"created_after": since.isoformat(),
}
)
response.raise_for_status()
return response.json()['data']

def create_deviation(test_result: Dict) -> Dict:
"""Create a deviation in BIO-QMS"""
severity = "CRITICAL" if test_result['deviation_percentage'] > 20 else "MAJOR"

deviation_data = {
"title": f"Out-of-Spec: {test_result['test_name']} - Batch {test_result['batch_number']}",
"description": f"""
Out-of-specification test result detected:

Test: {test_result['test_name']}
Batch: {test_result['batch_number']}
Measured Value: {test_result['measured_value']} {test_result['unit']}
Specification: {test_result['spec_min']} - {test_result['spec_max']} {test_result['unit']}
Deviation: {test_result['deviation_percentage']}%
Tested By: {test_result['technician_name']}
Test Date: {test_result['tested_at']}

LIMS Reference: {test_result['id']}
""".strip(),
"severity": severity,
}

response = requests.post(
"https://api.bio-qms.com/v1/deviations",
headers={
"Authorization": f"Bearer {BIO_QMS_API_KEY}",
"Content-Type": "application/json"
},
json=deviation_data
)
response.raise_for_status()
return response.json()

def update_lims_with_deviation_link(test_result_id: str, deviation_id: str, deviation_number: str):
"""Update LIMS test result with BIO-QMS deviation reference"""
response = requests.patch(
f"{LIMS_API_URL}/test-results/{test_result_id}",
headers={"Authorization": f"Bearer {LIMS_API_KEY}"},
json={
"qms_deviation_id": deviation_id,
"qms_deviation_number": deviation_number,
"qms_url": f"https://app.bio-qms.com/deviations/{deviation_id}"
}
)
response.raise_for_status()

def sync_loop(poll_interval_seconds: int = 300):
"""Main sync loop - poll LIMS every 5 minutes"""
last_check = datetime.now() - timedelta(hours=1) # Initial lookback

while True:
try:
print(f"Checking for out-of-spec results since {last_check}")

results = fetch_out_of_spec_results(since=last_check)
print(f"Found {len(results)} out-of-spec results")

for result in results:
# Check if already linked to a deviation
if result.get('qms_deviation_id'):
print(f"Result {result['id']} already has deviation {result['qms_deviation_number']}")
continue

# Create deviation
deviation = create_deviation(result)
print(f"Created deviation {deviation['deviationNumber']} for result {result['id']}")

# Link back to LIMS
update_lims_with_deviation_link(
result['id'],
deviation['id'],
deviation['deviationNumber']
)

last_check = datetime.now()
time.sleep(poll_interval_seconds)

except Exception as e:
print(f"Error in sync loop: {e}")
time.sleep(60) # Wait 1 minute before retry

if __name__ == "__main__":
sync_loop()

cURL Example

# Manually create a deviation from LIMS out-of-spec result
curl -X POST "https://api.bio-qms.com/v1/deviations" \
-H "Authorization: Bearer bio_qms_sk_live_abcdefghijklmnopqrstuvwxyz123456" \
-H "Content-Type: application/json" \
-d '{
"title": "Out-of-Spec: pH Test - Batch BT-2026-042",
"description": "pH measurement of 7.8 exceeds specification range of 6.5-7.5 by 4%",
"severity": "MAJOR"
}'

Recipe 3: Document Import

Scenario: Bulk import existing documents from legacy system or file storage.

Python Bulk Import Script

# bulk_import.py - Import documents from CSV
import csv
import requests
from pathlib import Path
from typing import Dict, List

BIO_QMS_API_KEY = "bio_qms_sk_live_abcdefghijklmnopqrstuvwxyz123456"

def read_csv_documents(csv_path: str) -> List[Dict]:
"""Read documents from CSV file"""
documents = []
with open(csv_path, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
documents.append(row)
return documents

def create_document(doc_data: Dict) -> Dict:
"""Create a document in BIO-QMS"""
# Map CSV columns to API fields
payload = {
"title": doc_data['title'],
"type": doc_data['type'],
"description": doc_data.get('description', ''),
"departmentId": doc_data['department_id'],
"effectiveDate": doc_data.get('effective_date'),
"tags": doc_data.get('tags', '').split(',') if doc_data.get('tags') else []
}

response = requests.post(
"https://api.bio-qms.com/v1/documents",
headers={
"Authorization": f"Bearer {BIO_QMS_API_KEY}",
"Content-Type": "application/json"
},
json=payload
)
response.raise_for_status()
return response.json()

def upload_document_file(document_id: str, file_path: str):
"""Upload PDF file for document"""
with open(file_path, 'rb') as f:
response = requests.post(
f"https://api.bio-qms.com/v1/documents/{document_id}/files",
headers={"Authorization": f"Bearer {BIO_QMS_API_KEY}"},
files={"file": f}
)
response.raise_for_status()

def bulk_import(csv_path: str, files_directory: str):
"""Import documents from CSV and upload associated files"""
documents = read_csv_documents(csv_path)
print(f"Importing {len(documents)} documents...")

success_count = 0
error_count = 0

for i, doc_data in enumerate(documents, 1):
try:
print(f"[{i}/{len(documents)}] Importing: {doc_data['title']}")

# Create document
document = create_document(doc_data)
print(f" Created: {document['documentNumber']}")

# Upload file if specified
if doc_data.get('file_name'):
file_path = Path(files_directory) / doc_data['file_name']
if file_path.exists():
upload_document_file(document['id'], str(file_path))
print(f" Uploaded: {doc_data['file_name']}")
else:
print(f" WARNING: File not found: {file_path}")

success_count += 1

except Exception as e:
print(f" ERROR: {e}")
error_count += 1

print(f"\nImport complete: {success_count} successful, {error_count} errors")

if __name__ == "__main__":
# Example CSV format:
# title,type,description,department_id,effective_date,tags,file_name
# "SOP-001: Manufacturing","SOP","Process validation","550e8400-e29b-41d4-a716-446655440000","2026-01-01T00:00:00Z","manufacturing,validation","SOP-001.pdf"

bulk_import(
csv_path="documents_to_import.csv",
files_directory="./legacy_documents/"
)

Example CSV File

title,type,description,department_id,effective_date,tags,file_name
"SOP-001: Manufacturing Process Validation","SOP","Defines the manufacturing validation process","550e8400-e29b-41d4-a716-446655440000","2026-01-01T00:00:00Z","manufacturing,validation","SOP-001.pdf"
"PROTO-042: Cleaning Validation Protocol","PROTOCOL","Protocol for equipment cleaning validation","550e8400-e29b-41d4-a716-446655440000","2026-02-01T00:00:00Z","cleaning,validation","PROTO-042.pdf"
"RPT-2026-001: Annual Quality Metrics","REPORT","Annual quality metrics report for 2025","660e8400-e29b-41d4-a716-446655440001","2026-01-15T00:00:00Z","quality,metrics,annual","RPT-2026-001.pdf"

Recipe 4: Custom Approval Workflows

Scenario: Build a multi-stage approval workflow with custom business logic.

TypeScript Custom Workflow Engine

// workflow-engine.ts - Custom document approval workflow
import { BioQMSClient } from '@bio-qms/api-client';
import { sendEmail, sendSlackNotification } from './notifications';

const client = new BioQMSClient({
apiKey: 'bio_qms_sk_live_abcdefghijklmnopqrstuvwxyz123456',
});

interface ApprovalStage {
name: string;
approverRole: string;
requiredCount: number;
autoEscalateHours?: number;
}

const DOCUMENT_APPROVAL_WORKFLOW: ApprovalStage[] = [
{
name: 'Peer Review',
approverRole: 'REVIEWER',
requiredCount: 2,
autoEscalateHours: 72,
},
{
name: 'Department Manager Approval',
approverRole: 'QUALITY_MANAGER',
requiredCount: 1,
autoEscalateHours: 48,
},
{
name: 'QA Final Approval',
approverRole: 'APPROVER',
requiredCount: 1,
autoEscalateHours: 24,
},
];

async function initiateApprovalWorkflow(documentId: string) {
const document = await client.documents.get(documentId);

// Start with first stage
const firstStage = DOCUMENT_APPROVAL_WORKFLOW[0];

// Find reviewers
const reviewers = await client.users.list({
role: firstStage.approverRole,
departmentId: document.departmentId,
isActive: true,
});

// Create review tasks
for (const reviewer of reviewers.data.slice(0, firstStage.requiredCount)) {
await createReviewTask(documentId, reviewer.id, firstStage.name);
await sendEmail({
to: reviewer.email,
subject: `Review Required: ${document.title}`,
body: `Please review document ${document.documentNumber}: ${document.title}`,
});
}

// Schedule escalation check
setTimeout(
() => checkEscalation(documentId, 0),
firstStage.autoEscalateHours! * 3600 * 1000
);
}

async function handleApproval(documentId: string, userId: string, approved: boolean) {
const document = await client.documents.get(documentId);
const reviewTasks = await getReviewTasks(documentId);

// Record approval/rejection
await recordReviewDecision(documentId, userId, approved);

if (!approved) {
// Rejection - send back to author
await sendEmail({
to: document.createdBy,
subject: `Document Rejected: ${document.title}`,
body: `Your document has been rejected. Please review feedback and resubmit.`,
});
return;
}

// Check if current stage is complete
const currentStageIndex = getCurrentStageIndex(reviewTasks);
const currentStage = DOCUMENT_APPROVAL_WORKFLOW[currentStageIndex];
const approvalCount = reviewTasks.filter(
(t) => t.stage === currentStage.name && t.decision === 'approved'
).length;

if (approvalCount >= currentStage.requiredCount) {
// Stage complete - advance to next stage
const nextStageIndex = currentStageIndex + 1;

if (nextStageIndex >= DOCUMENT_APPROVAL_WORKFLOW.length) {
// All stages complete - finalize approval
await client.documents.approve(documentId);
await sendSlackNotification({
channel: '#quality-approvals',
message: `✅ Document ${document.documentNumber} approved: ${document.title}`,
});
} else {
// Start next stage
await startApprovalStage(documentId, nextStageIndex);
}
}
}

async function startApprovalStage(documentId: string, stageIndex: number) {
const document = await client.documents.get(documentId);
const stage = DOCUMENT_APPROVAL_WORKFLOW[stageIndex];

const approvers = await client.users.list({
role: stage.approverRole,
departmentId: document.departmentId,
isActive: true,
});

for (const approver of approvers.data.slice(0, stage.requiredCount)) {
await createReviewTask(documentId, approver.id, stage.name);
await sendEmail({
to: approver.email,
subject: `Approval Required (${stage.name}): ${document.title}`,
body: `Please approve document ${document.documentNumber}: ${document.title}`,
});
}

if (stage.autoEscalateHours) {
setTimeout(
() => checkEscalation(documentId, stageIndex),
stage.autoEscalateHours * 3600 * 1000
);
}
}

async function checkEscalation(documentId: string, stageIndex: number) {
const reviewTasks = await getReviewTasks(documentId);
const stage = DOCUMENT_APPROVAL_WORKFLOW[stageIndex];

const pendingTasks = reviewTasks.filter(
(t) => t.stage === stage.name && t.decision === null
);

if (pendingTasks.length > 0) {
// Escalate to managers
const document = await client.documents.get(documentId);
await sendSlackNotification({
channel: '#quality-escalations',
message: `⚠️ Document ${document.documentNumber} stuck at ${stage.name} stage`,
});

// Send reminder emails
for (const task of pendingTasks) {
const user = await client.users.get(task.userId);
await sendEmail({
to: user.email,
subject: `URGENT: Overdue Approval - ${document.title}`,
body: `Your approval for ${document.documentNumber} is overdue.`,
});
}
}
}

// Helper functions (simplified)
async function createReviewTask(documentId: string, userId: string, stage: string) {
// Store in your database
console.log(`Created review task: ${documentId} -> ${userId} (${stage})`);
}

async function recordReviewDecision(documentId: string, userId: string, approved: boolean) {
console.log(`Recorded decision: ${documentId} -> ${userId} = ${approved}`);
}

async function getReviewTasks(documentId: string): Promise<any[]> {
// Fetch from your database
return [];
}

function getCurrentStageIndex(reviewTasks: any[]): number {
// Determine current stage based on completed tasks
return 0;
}

Postman Collection

A complete Postman collection is available for testing:

Collection Structure:

BIO-QMS API
├── Authentication
│ ├── Login (JWT)
│ ├── OAuth2 Authorization
│ └── Refresh Token
├── Documents
│ ├── List Documents
│ ├── Get Document
│ ├── Create Document
│ ├── Update Document
│ ├── Delete Document
│ └── Upload File
├── CAPAs
│ ├── List CAPAs
│ ├── Create CAPA
│ ├── Add Action
│ └── Complete CAPA
├── Deviations
│ ├── List Deviations
│ ├── Report Deviation
│ └── Escalate Deviation
├── Users
│ ├── List Users
│ ├── Get User
│ └── Create User
├── Audit Trail
│ ├── Query Audit Trail
│ └── Export Audit Trail
└── Webhooks
├── Create Webhook
├── List Webhooks
├── Test Webhook
└── Get Webhook Logs

Download: BIO-QMS-API-Postman-Collection.json

Environment Variables:

{
"base_url": "https://api.bio-qms.com/v1",
"auth_url": "https://auth.bio-qms.com",
"email": "api-user@yourcompany.com",
"password": "your_secure_password",
"access_token": "{{access_token}}",
"api_key": "bio_qms_sk_live_abcdefghijklmnopqrstuvwxyz123456"
}

Additional Resources

Rate Limiting

All API endpoints are rate limited:

TierRequests/HourBurst
Free1,00050
Pro10,000200
Enterprise100,0001,000

Rate limit headers:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 950
X-RateLimit-Reset: 1708617600

Error Codes

CodeMeaning
400Bad Request - Validation error
401Unauthorized - Invalid/missing token
403Forbidden - Insufficient permissions
404Not Found - Resource doesn't exist
409Conflict - Resource state conflict
422Unprocessable Entity - Business logic error
429Too Many Requests - Rate limit exceeded
500Internal Server Error
503Service Unavailable - Maintenance mode

Support

Changelog

  • 2026-02-17: v1.0.0 - Initial API documentation release
  • 2026-02-01: Beta API launched
  • 2026-01-15: Developer preview

Document Status: Active Last Updated: 2026-02-17 Version: 1.0.0 Maintained By: BIO-QMS Documentation Team