API Security
Complete guide to securing API integrations with Cothon including JWT authentication, API tokens, rate limiting, CORS policies, webhook security, and best practices
API Security
Cothon's REST API provides programmatic access to procurement intelligence data, enabling integrations with your existing business systems, custom dashboards, and automated workflows. Security is paramount when exposing sensitive bid data via API, so we've implemented multiple layers of protection: robust authentication, granular authorization, rate limiting, and comprehensive audit logging.
This guide covers everything you need to securely integrate with Cothon's API, from generating and managing API tokens to implementing webhook security and handling errors gracefully.
API Authentication
All Cothon API endpoints require authentication. We support two authentication methods depending on your use case.
JWT Token Authentication (User Sessions)
When users interact with the Cothon web application, authentication uses JSON Web Tokens (JWT) issued during login.
Note
When to Use JWT Authentication: Use JWT tokens for web applications and mobile apps where users log in interactively. JWTs provide session-based access tied to specific user identities.
For server-to-server integrations, scripts, or CI/CD pipelines where there's no interactive user, use API Token Authentication instead.
API Token Authentication (Service Accounts)
API tokens provide programmatic access for automated systems, scripts, and integrations without requiring user credentials.
Warning
Token Compromise Response: If you suspect an API token has been compromised:
- Revoke Immediately: Settings → Security → API Tokens → Revoke the compromised token
- Review Audit Logs: Check Settings → Security → Audit Logs for suspicious activity using that token
- Generate New Token: Create a replacement token with the same permissions
- Investigate: Determine how the token was compromised (Git history, logs, server breach)
- Notify Security Team: If the compromise affected sensitive data, contact your usual support channel
Compromised tokens can be used to access all data within your organization until revoked. Act quickly.
Token Format & Structure
JWT Access Tokens follow the standard <header>.<payload>.<signature> format. The backend verifies the cryptographic signature on every request and pins the algorithm to a strict allowlist to prevent algorithm confusion attacks — unsigned tokens are rejected.
API Tokens use a prefixed format (cothon_live_...) that enables automatic detection by secret scanning tools. Tokens are cryptographically random and long enough to resist brute-force attacks.
Never use live tokens in development/testing environments or commit them to version control.
API Authorization
Authentication verifies who you are; authorization determines what you can do.
Row-Level Security (RLS)
Cothon uses PostgreSQL Row-Level Security to enforce data access policies at the database level.
Every table has RLS policies that filter data based on the authenticated user's identity and organization memberships. For example, read policies ensure users can only access data belonging to organizations they are members of, and write/delete policies enforce role requirements (admin or owner) for destructive operations.
Benefits of RLS:
- Defense in Depth: Even if application-level access controls are bypassed, the database enforces policies independently
- Automatic Enforcement: Every query is automatically filtered to the caller's authorized data
- Consistent Isolation: Organization data boundaries are enforced at the database layer, not just in application code
Permission Model
API access is controlled by organization roles:
| Endpoint | Member | Admin | Owner |
|---|---|---|---|
| GET /api/v1/bid-analyses | ✓ | ✓ | ✓ |
| POST /api/v1/bid-analysis/analyze | ✓ | ✓ | ✓ |
| PUT /api/v1/bid-analyses/:id | ✓ | ✓ | ✓ |
| DELETE /api/v1/bid-analyses/:id | ✓ | ✓ | ✓ |
| GET /api/v1/opportunities | ✓ | ✓ | ✓ |
| POST /api/v1/opportunities/:id/analyze | ✓ | ✓ | ✓ |
| POST /api/v1/ai/generate-proposal | ✓ | ✓ | ✓ |
| GET /api/v1/export/organization | ✗ | ✓ | ✓ |
| POST /api/v1/sharing/shared-analyses | ✓ | ✓ | ✓ |
| GET /api/v1/audit-logs | ✗ | ✓ | ✓ |
| POST /api/v1/organization/members | ✗ | ✓ | ✓ |
| DELETE /api/v1/organization/members/:id | ✗ | ✓ | ✓ |
| DELETE /api/v1/organization | ✗ | ✗ | ✓ |
API Token Scope Mapping:
- Read-Only Tokens: GET requests only
- Read-Write Tokens: Equivalent to Member role (GET, POST, PUT, PATCH)
- Admin Tokens: Equivalent to Admin role (includes DELETE, user management)
How API Tokens Enforce RLS: API tokens are exchanged for short-lived, scoped credentials internally. All subsequent database queries run under Row-Level Security exactly as if the user had logged in interactively — the API token does not bypass data isolation. The raw token is never persisted; only a hash is stored.
Error Responses
When authorization fails, the API returns clear error responses:
403 Forbidden: You are authenticated but do not have the required role for this operation. The response includes a message explaining the required permission level.
401 Unauthorized: Your token is invalid, expired, or missing. Re-authenticate and try again.
404 Not Found: The resource does not exist or you do not have access to it. We return 404 (not 403) when you try to access resources from other organizations, to prevent leaking information about whether resources exist.
Tip
Debugging Authorization Errors: If you're getting 403 errors unexpectedly:
- Check Your Role: Verify your organization role at Settings → Organization → Team Members
- Check Token Scope: Verify API token scope at Settings → Security → API Tokens
- Review Audit Logs: Check Settings → Security → Audit Logs for failed permission checks
- Test with Web UI: If the operation works in the web UI but fails via API, contact support@cothon.ca
Common issues:
- Using read-only token for write operations
- Member role trying to delete items (requires Admin)
- Accessing resources from organizations you're not a member of
Rate Limiting
To ensure fair usage and prevent abuse, Cothon implements two layers of rate limiting:
- Layer 1 (IP-based): A global rate limit per IP address provides basic protection against abuse.
- Layer 2 (Plan-aware): Per-tenant rate limits based on subscription plan. The backend identifies the caller's organization and plan tier from their authenticated session, then applies the appropriate limit.
Rate Limit Tiers
Rate limits vary by subscription plan and endpoint type:
| Plan | Standard Endpoints | AI/Compute-Intensive Endpoints | Burst Allowance |
|---|---|---|---|
| Free | 60 req/min, 1,000 req/day | 5 req/min, 50 req/day | 10 requests |
| Professional | 300 req/min, 10,000 req/day | 20 req/min, 200 req/day | 50 requests |
| Enterprise | 1,000 req/min, 50,000 req/day | 100 req/min, 1,000 req/day | 200 requests |
| Custom | Negotiable | Negotiable | Negotiable |
Standard Endpoints: GET, POST, PUT, DELETE for bid analyses, proposals, opportunities, organization data
AI/Compute-Intensive Endpoints:
/api/v1/bid-analysis/analyze(PDF analysis)/api/v1/ai/generate-proposal(proposal generation)/api/v1/full-extraction/extract(full document extraction)/api/v1/rag/search(semantic search)
Burst Allowance: Short-term spike in requests beyond the per-minute limit. Once exhausted, requests are throttled to the per-minute rate.
Rate Limit Headers
Every API response includes rate limit headers:
HTTP/1.1 200 OK
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 1711800120
X-RateLimit-Bucket: standard
Retry-After: 42
| Header | Description | Example |
|---|---|---|
X-RateLimit-Limit | Maximum requests per window | 300 (requests per minute) |
X-RateLimit-Remaining | Requests remaining in current window | 287 |
X-RateLimit-Reset | Unix timestamp when limit resets | 1711800120 (epoch seconds) |
X-RateLimit-Bucket | Rate limit category | standard, ai-compute |
Retry-After | Seconds until you can retry (only on 429 errors) | 42 |
Handling Rate Limits
When you exceed rate limits, the API returns 429 Too Many Requests:
{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded. You've made 301 requests in the current minute. Limit: 300 requests per minute.",
"code": "RATE_LIMIT_EXCEEDED",
"retry_after_seconds": 42,
"limit": 300,
"window": "1 minute",
"reset_at": "2026-03-30T14:25:20Z"
}
Success
Rate Limit Increases: If your legitimate use case requires higher rate limits, contact your usual support channel with details:
- Your current plan and API usage
- Description of your integration
- Expected request volume (requests per minute/day)
- Specific endpoints you're calling
We can provide custom rate limits for enterprise customers and high-volume integrations.
Rate Limiting Best Practices
CORS Policies
Cross-Origin Resource Sharing (CORS) controls which web origins can access the Cothon API from browsers.
Allowed Origins
By default, Cothon allows CORS requests from:
https://app.cothon.ca(production frontend)https://sandbox.cothon.ca(sandbox frontend)http://localhost:3000(local development)http://localhost:5173(Vite development server)
Custom Origins (Enterprise): Enterprise plans can configure additional allowed origins at Settings → Integrations → CORS Settings.
Example use cases:
- Custom dashboards hosted at
https://dashboard.acmecorp.com - Internal tools at
https://tools.acmecorp.internal - Partner portals at
https://partners.acmecorp.com
CORS Headers
When making API requests from browsers, Cothon returns CORS headers:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.cothon.ca
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-ID
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
| Header | Description | Value |
|---|---|---|
Access-Control-Allow-Origin | Allowed origin for this request | https://app.cothon.ca |
Access-Control-Allow-Methods | Allowed HTTP methods | GET, POST, PUT, PATCH, DELETE, OPTIONS |
Access-Control-Allow-Headers | Allowed request headers | Authorization, Content-Type, X-Request-ID |
Access-Control-Allow-Credentials | Allow cookies/credentials | true |
Access-Control-Max-Age | How long to cache preflight response | 86400 (24 hours) |
Preflight Requests
For complex requests (POST, PUT, DELETE, custom headers), browsers send a preflight OPTIONS request:
OPTIONS /api/v1/bid-analyses HTTP/1.1
Origin: https://dashboard.acmecorp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
Cothon responds with allowed methods and headers:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://dashboard.acmecorp.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-ID
Access-Control-Max-Age: 86400
The browser then proceeds with the actual request.
Warning
CORS and Security: CORS is a browser security feature. It does NOT prevent non-browser clients (curl, Postman, server-side scripts) from accessing the API.
For server-side integrations, CORS is irrelevant—authentication (API tokens) is the primary security control.
CORS only protects against malicious websites making unauthorized API requests from users' browsers.
Handling CORS Errors
If you encounter CORS errors in browser-based applications:
Error: Access to fetch at 'https://api.cothon.ca/api/v1/bid-analyses' from origin 'https://myapp.com' has been blocked by CORS policy
Solutions:
Webhook Security
Webhooks allow Cothon to notify your systems of events (new analyses, proposal generation complete, opportunity matches) without polling.
Webhook Setup
Webhook Event Types
Cothon sends webhooks for these events:
| Event | Description | Payload |
|---|---|---|
analysis.created | New bid analysis started | analysis_id, title, status, created_at |
analysis.completed | Bid analysis finished | analysis_id, title, requirements_count, overall_score |
analysis.failed | Bid analysis failed | analysis_id, error_message, failed_at |
proposal.generated | AI proposal generated | proposal_id, analysis_id, word_count, generated_at |
opportunity.matched | New opportunity matches capabilities | opportunity_id, title, deadline, match_score |
opportunity.deadline_soon | Saved opportunity deadline approaching | opportunity_id, title, deadline, days_remaining |
member.added | Team member added to organization | user_id, email, role, added_at |
member.removed | Team member removed from organization | user_id, email, removed_at |
Webhook Security
Webhook Retry Logic
If your endpoint returns an error (non-200 response or timeout), Cothon retries the webhook:
| Attempt | Delay | Total Time |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 30 seconds | 30s |
| 3 | 5 minutes | 5m 30s |
| 4 | 30 minutes | 35m 30s |
| 5 | 2 hours | 2h 35m 30s |
After 5 failed attempts, the webhook is marked as failed and you'll receive an email alert.
View Failed Webhooks: Navigate to Settings → Integrations → Webhooks → "Failed Deliveries" to see webhooks that failed all retries.
You can manually retry failed webhooks by clicking "Retry Delivery".
Tip
Webhook Testing: Use tools like ngrok to expose local development servers for webhook testing:
# Start local server on port 5000
python app.py
# Expose via ngrok
ngrok http 5000
Use the ngrok URL (e.g., https://abc123.ngrok.io/webhooks/cothon) as your webhook endpoint in Cothon settings.
Alternatively, use webhook testing services like webhook.site to inspect webhook payloads without writing code.
API Best Practices
Error Handling
Implement comprehensive error handling for all API requests:
import requests
def make_api_request(url, headers, max_retries=3):
for attempt in range(max_retries):
try:
response = requests.get(url, headers=headers, timeout=30)
if response.status_code == 200:
return response.json()
elif response.status_code == 401:
raise AuthenticationError("Invalid or expired API token")
elif response.status_code == 403:
raise PermissionError(f"Insufficient permissions: {response.json().get('message')}")
elif response.status_code == 404:
return None # Resource not found
elif response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 60))
time.sleep(retry_after)
continue
elif response.status_code >= 500:
# Server error, retry with backoff
wait_time = 2 ** attempt
time.sleep(wait_time)
continue
else:
response.raise_for_status()
except requests.exceptions.Timeout:
if attempt == max_retries - 1:
raise TimeoutError(f"Request timed out after {max_retries} attempts")
time.sleep(2 ** attempt)
except requests.exceptions.ConnectionError:
if attempt == max_retries - 1:
raise ConnectionError("Could not connect to Cothon API")
time.sleep(2 ** attempt)
raise Exception(f"Request failed after {max_retries} attempts")
Logging & Monitoring
Log all API interactions for debugging and monitoring:
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def make_api_request(url, headers):
logger.info(f"API Request: GET {url}")
try:
response = requests.get(url, headers=headers, timeout=30)
logger.info(f"API Response: {response.status_code} - {len(response.content)} bytes")
if response.status_code != 200:
logger.warning(f"API Error: {response.status_code} - {response.text}")
return response
except Exception as e:
logger.error(f"API Exception: {str(e)}", exc_info=True)
raise
Metrics to Track:
- Request count by endpoint
- Success/error rates
- Response times (p50, p95, p99)
- Rate limit usage
- API token usage by token
Use monitoring tools like Prometheus, Datadog, or New Relic to track API health.
Request IDs
Include a unique request ID in every API call for debugging:
import uuid
def make_api_request(url, headers):
request_id = str(uuid.uuid4())
headers['X-Request-ID'] = request_id
logger.info(f"Request {request_id}: GET {url}")
response = requests.get(url, headers=headers)
logger.info(f"Request {request_id}: {response.status_code}")
return response
If you encounter errors, provide the X-Request-ID to support for faster troubleshooting.
Pagination
Use pagination for endpoints that return large result sets:
def fetch_all_analyses(headers):
analyses = []
page = 1
per_page = 100
while True:
response = requests.get(
'https://api.cothon.ca/api/v1/bid-analyses',
headers=headers,
params={'page': page, 'per_page': per_page}
)
data = response.json()
analyses.extend(data['items'])
if not data.get('has_more'):
break
page += 1
return analyses
Pagination Limits:
- Maximum
per_page: 100 items - Default
per_page: 20 items
Use per_page=100 to minimize API calls.
Frequently Asked Questions
Additional Resources
- Authentication & Access Control - User authentication and 2FA setup
- Security Overview - Comprehensive security architecture
- API Reference - Complete API endpoint documentation
- Integration Guides - Step-by-step integration tutorials
API Support:
- Technical questions: support@cothon.ca
- Security questions: your usual support channel
- Enterprise API access: your usual support channel
Developer Resources:
- API Status: (contact your account representative for current status)
- API Changelog: https://docs.cothon.ca/api/changelog
- Developer Community: https://community.cothon.ca
Last Updated: March 30, 2026
Next Review: June 30, 2026
Related Articles
Was this page helpful?