C
Docs

API Security

Complete guide to securing API integrations with Cothon including JWT authentication, API tokens, rate limiting, CORS policies, webhook security, and best practices

Updated 2026-04-1327 min read

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:

  1. Revoke Immediately: Settings → Security → API Tokens → Revoke the compromised token
  2. Review Audit Logs: Check Settings → Security → Audit Logs for suspicious activity using that token
  3. Generate New Token: Create a replacement token with the same permissions
  4. Investigate: Determine how the token was compromised (Git history, logs, server breach)
  5. 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:

EndpointMemberAdminOwner
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:

  1. Check Your Role: Verify your organization role at Settings → Organization → Team Members
  2. Check Token Scope: Verify API token scope at Settings → Security → API Tokens
  3. Review Audit Logs: Check Settings → Security → Audit Logs for failed permission checks
  4. 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:

  1. Layer 1 (IP-based): A global rate limit per IP address provides basic protection against abuse.
  2. 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:

PlanStandard EndpointsAI/Compute-Intensive EndpointsBurst Allowance
Free60 req/min, 1,000 req/day5 req/min, 50 req/day10 requests
Professional300 req/min, 10,000 req/day20 req/min, 200 req/day50 requests
Enterprise1,000 req/min, 50,000 req/day100 req/min, 1,000 req/day200 requests
CustomNegotiableNegotiableNegotiable

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
HeaderDescriptionExample
X-RateLimit-LimitMaximum requests per window300 (requests per minute)
X-RateLimit-RemainingRequests remaining in current window287
X-RateLimit-ResetUnix timestamp when limit resets1711800120 (epoch seconds)
X-RateLimit-BucketRate limit categorystandard, ai-compute
Retry-AfterSeconds 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
HeaderDescriptionValue
Access-Control-Allow-OriginAllowed origin for this requesthttps://app.cothon.ca
Access-Control-Allow-MethodsAllowed HTTP methodsGET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-HeadersAllowed request headersAuthorization, Content-Type, X-Request-ID
Access-Control-Allow-CredentialsAllow cookies/credentialstrue
Access-Control-Max-AgeHow long to cache preflight response86400 (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:

EventDescriptionPayload
analysis.createdNew bid analysis startedanalysis_id, title, status, created_at
analysis.completedBid analysis finishedanalysis_id, title, requirements_count, overall_score
analysis.failedBid analysis failedanalysis_id, error_message, failed_at
proposal.generatedAI proposal generatedproposal_id, analysis_id, word_count, generated_at
opportunity.matchedNew opportunity matches capabilitiesopportunity_id, title, deadline, match_score
opportunity.deadline_soonSaved opportunity deadline approachingopportunity_id, title, deadline, days_remaining
member.addedTeam member added to organizationuser_id, email, role, added_at
member.removedTeam member removed from organizationuser_id, email, removed_at

Webhook Security

Webhook Retry Logic

If your endpoint returns an error (non-200 response or timeout), Cothon retries the webhook:

AttemptDelayTotal Time
1Immediate0s
230 seconds30s
35 minutes5m 30s
430 minutes35m 30s
52 hours2h 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

API Support:

  • Technical questions: support@cothon.ca
  • Security questions: your usual support channel
  • Enterprise API access: your usual support channel

Developer Resources:


Last Updated: March 30, 2026

Next Review: June 30, 2026

Was this page helpful?

API Security | Cothon Docs | Cothon