API Reference

    PolicyForge API

    Generate compliant policies, record and audit consent across every surface (cookie banner, hosted-policy "I Agree", and your own backend), and ship in minutes.

    Quickstart

    Three minutes to your first call. The API uses bearer-token auth, JSON bodies, and standard HTTP status codes.

    1. 1

      Get your API key

      Visit API Dashboard and click Generate key. Keys are prefixed pf_. Treat them like passwords — never commit them to git or expose them on the client.

    2. 2

      Make your first request

      List your policies. Replace $POLICYFORGE_API_KEY with your real key.

      curl https://policyforge.co/api/v1/policies \
        -H "Authorization: Bearer $POLICYFORGE_API_KEY"
    3. 3

      Read the response

      Successful responses return 200 OK with a JSON body. Errors return a 4xx/5xx with { error, details? }. See Errors for the full shape and Rate limits for the headers we return on every request.

    Authentication

    All endpoints under /api/v1/* require a bearer token, except /api/v1/consent/policy/{id}/record which is intentionally public for hosted-policy I-Agree clicks.

    Generate keys from your API Dashboard. Each key is prefixed pf_ and grants access to data owned by your account.

    • Server-side only. Never embed an API key in client JavaScript. Use the public consent endpoint or a backend proxy.
    • Rotate freely. Generate a new key, deploy the update, then revoke the old one — both keys work concurrently.
    • Scoped automatically. Reads return only your account's data. Writes attribute records to your account.
    • Compromised? Revoke immediately from the dashboard and regenerate.
    Sending the bearer token
    curl https://policyforge.co/api/v1/policies \
      -H "Authorization: Bearer pf_YOUR_API_KEY"

    Errors

    Errors use standard HTTP status codes. The response body is always JSON with an error field; some validation errors include a details field with the offending value.

    Status codes

    CodeMeaning
    400
    Bad Request
    The request body or query parameters are malformed (missing required fields, invalid JSON, bad UUID, etc.). The error message names the offending field.
    401
    Unauthorized
    Missing or invalid API key. Check that the Authorization header is `Bearer pf_…` and the key hasn't been revoked.
    403
    Forbidden
    Authenticated but the operation isn't allowed — typically because the resource isn't yours or a precondition isn't met (e.g. policy not hosted).
    404
    Not Found
    Resource doesn't exist or isn't owned by your API key. We deliberately don't disclose existence across owners.
    405
    Method Not Allowed
    HTTP method isn't supported on this endpoint.
    429
    Too Many Requests
    Rate limit exceeded. The response includes `retry_after` (seconds) and the `X-RateLimit-Reset` header.
    500
    Internal Server Error
    Something broke on our side. These are tracked automatically. Retry with exponential backoff is safe for idempotent operations (GET, PATCH with same body, DELETE).

    Error body shape

    // Errors always return a JSON body of this shape:
    {
      "error": "policy_id not found or not owned by this API key",
      "details": "..." // optional, present on validation errors
    }
    
    // Defensive client pattern:
    const res = await fetch(url, options);
    if (!res.ok) {
      const { error } = await res.json().catch(() => ({ error: res.statusText }));
      throw new Error(`${res.status}: ${error}`);
    }

    Rate limits

    Limits are per API key. Headers on every response tell you how much budget you have left, so you don't need to guess or count locally.

    TierPer minutePer day
    Free10100
    Pro605,000
    Enterprise30050,000

    429 responses are returned when the per-minute window is exceeded. The window resets continuously, not on a fixed clock minute.

    Headers + retry pattern
    // Every response includes:
    X-RateLimit-Limit: 60         // requests/min for your tier
    X-RateLimit-Remaining: 47     // remaining in the current window
    X-RateLimit-Reset: 1778187600 // Unix timestamp when window resets
    
    // On 429, we also return:
    Retry-After: 32               // seconds to wait before retrying
    
    // Defensive client pattern:
    if (res.status === 429) {
      const retryAfter = Number(res.headers.get("retry-after") ?? "30");
      await new Promise((r) => setTimeout(r, retryAfter * 1000));
      // ...retry once
    }

    Policies

    Generate, list, fetch, update, and delete policies. Generated policies are automatically saved and (when hosting is enabled) served at a public URL.

    POST/api/v1/policies

    Generate a compliance policy with AI.

    Returns a fully-formatted policy and saves it to your account. Generation typically takes 8–20 seconds depending on the policy type and jurisdiction. The response includes a hosted_url you can link to immediately.

    Generation counts against your monthly policy quota (Free: 2 / month, calendar reset).

    Headers

    FieldTypeDescription
    AuthorizationrequiredstringBearer pf_…

    Body parameters

    FieldTypeDescription
    typerequiredenumOne of privacy_policy, terms_of_service, cookie_policy, refund_policy, eula, disclaimer.
    business_typerequiredstringYour business category — e.g. saas, e-commerce, mobile_app, healthcare, fintech, education.
    jurisdictionrequiredstringPrimary legal regime — e.g. gdpr (EU/UK), ccpa (California), pipeda (Canada), lgpd (Brazil). Multi-jurisdiction policies can list multiple, comma-separated.
    company_namerequiredstringLegal entity name to use throughout the document.
    contact_emailrequiredstringContact email for privacy / legal queries. Surfaced verbatim in the policy.
    website_urlstringYour primary website URL.
    data_collectionstring[]Categories of data you collect — e.g. ["email", "name", "ip_address", "payment_info", "analytics"].
    third_party_integrationsstring[]Names of third parties that process user data — e.g. ["Stripe", "Google Analytics", "Mixpanel"].
    user_accountsbooleanDo users sign up / authenticate?
    paymentsbooleanDo you process payments?
    marketingbooleanDo you send marketing communications?
    analyticsbooleanDo you use behavioural analytics?
    cookiesbooleanDoes your site use cookies (any kind)?
    children_databooleanDo you knowingly collect data from anyone under 13? Triggers COPPA-aware language.
    Request
    curl -X POST https://policyforge.co/api/v1/policies \
      -H "Authorization: Bearer $POLICYFORGE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "type": "privacy_policy",
        "business_type": "saas",
        "jurisdiction": "gdpr",
        "company_name": "Acme Inc",
        "contact_email": "privacy@acme.com",
        "website_url": "https://acme.com",
        "data_collection": ["email", "name", "usage_data"],
        "third_party_integrations": ["Stripe", "Google Analytics"],
        "user_accounts": true,
        "payments": true,
        "analytics": true,
        "cookies": true
      }'
    Response200 Created
    {
      "id": "8331bf67-3748-471f-b595-9ba55a972cd5",
      "title": "Acme Inc Privacy Policy",
      "policy_type": "privacy_policy",
      "status": "published",
      "content": "...",
      "hosted_url": "https://policyforge.co/policy/acme-inc-privacy-policy-1",
      "hosting_enabled": true,
      "created_at": "2026-05-08T22:30:00.000+00:00",
      "updated_at": "2026-05-08T22:30:00.000+00:00"
    }
    Response400 Bad Request
    {
      "error": "Missing required field: jurisdiction"
    }
    Response402 Quota Exhausted
    {
      "error": "Monthly policy quota exhausted. Upgrade to continue.",
      "details": {
        "tier": "free",
        "limit": 2,
        "used": 2,
        "resets_at": "2026-06-01"
      }
    }
    GET/api/v1/policies

    List all policies you've generated.

    Headers

    FieldTypeDescription
    AuthorizationrequiredstringBearer pf_…

    Query parameters

    FieldTypeDescription
    limitinteger
    default: 20
    Page size, 1–100.
    offsetinteger
    default: 0
    Number of results to skip.
    statusenum("draft", "published", "archived")Filter by lifecycle status.
    typestringFilter by policy type, same values as POST.
    sortByenum("createdAt", "updatedAt", "name")
    default: "updatedAt"
    Sort field.
    sortOrderenum("asc", "desc")
    default: "desc"
    Sort direction.
    Request
    curl "https://policyforge.co/api/v1/policies?status=published&limit=50" \
      -H "Authorization: Bearer $POLICYFORGE_API_KEY"
    Response200 OK
    {
      "data": [
        {
          "id": "8331bf67-3748-471f-b595-9ba55a972cd5",
          "title": "Acme Inc Privacy Policy",
          "policy_type": "privacy_policy",
          "status": "published",
          "hosted_url": "https://policyforge.co/policy/acme-inc-privacy-policy-1",
          "views": 1284,
          "consent_count": 312,
          "updated_at": "2026-05-08T22:30:00.000+00:00"
        }
      ],
      "total": 1,
      "limit": 50,
      "offset": 0
    }
    GET/api/v1/policies/{id}

    Fetch a single policy by ID.

    Headers

    FieldTypeDescription
    AuthorizationrequiredstringBearer pf_…

    Path parameters

    FieldTypeDescription
    idrequireduuidPolicy UUID.
    Request
    curl https://policyforge.co/api/v1/policies/8331bf67-3748-471f-b595-9ba55a972cd5 \
      -H "Authorization: Bearer $POLICYFORGE_API_KEY"
    Response200 OK
    {
      "id": "8331bf67-3748-471f-b595-9ba55a972cd5",
      "title": "Acme Inc Privacy Policy",
      "policy_type": "privacy_policy",
      "status": "published",
      "content": "...",
      "hosted_url": "https://policyforge.co/policy/acme-inc-privacy-policy-1",
      "hosting_enabled": true,
      "created_at": "2026-05-08T22:30:00.000+00:00",
      "updated_at": "2026-05-08T22:30:00.000+00:00"
    }
    Response404 Not Found
    {
      "error": "Policy not found"
    }
    PATCH/api/v1/policies/{id}

    Update a policy. Bumps updated_at, which bumps the policy_version stamp on subsequent consents.

    Headers

    FieldTypeDescription
    AuthorizationrequiredstringBearer pf_…

    Path parameters

    FieldTypeDescription
    idrequireduuidPolicy UUID.

    Body parameters

    FieldTypeDescription
    titlestringDisplay title.
    statusenum("draft", "published", "archived")Lifecycle status. Setting to `archived` removes from listings.
    hosting_enabledbooleanToggle the public hosted URL on/off.
    contentstringMarkdown body. Replaces the entire policy text. Use sparingly — manual edits aren't re-validated against jurisdiction rules.
    Request
    curl -X PATCH https://policyforge.co/api/v1/policies/$POLICY_ID \
      -H "Authorization: Bearer $POLICYFORGE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "title": "Acme Privacy Policy (2026)", "status": "published" }'
    Response200 Updated
    {
      "id": "8331bf67-3748-471f-b595-9ba55a972cd5",
      "title": "Acme Privacy Policy (2026)",
      "status": "published",
      "updated_at": "2026-05-08T22:35:00.000+00:00"
    }
    DELETE/api/v1/policies/{id}

    Permanently delete a policy. Cascades to its analytics and consent records.

    Destructive. Consider PATCH with { "status": "archived" } if you want to hide the policy without losing the consent audit trail.

    Headers

    FieldTypeDescription
    AuthorizationrequiredstringBearer pf_…

    Path parameters

    FieldTypeDescription
    idrequireduuidPolicy UUID.
    Request
    curl -X DELETE https://policyforge.co/api/v1/policies/$POLICY_ID \
      -H "Authorization: Bearer $POLICYFORGE_API_KEY"
    Response200 Deleted
    {
      "deleted": true,
      "id": "8331bf67-3748-471f-b595-9ba55a972cd5"
    }
    Response404 Not Found
    {
      "error": "Policy not found"
    }

    Webhooks

    We POST events to your URL as soon as they happen. Configure endpoints and pick events from your API Dashboard. Every delivery is signed; verify the signature before trusting the payload.

    Available events

    EventFires when
    policy.createdA new policy was generated.
    policy.updatedA policy's content or metadata was changed.
    policy.publishedA policy was set to status='published'.
    policy.archivedA policy was set to status='archived'.
    consent.recordedA consent event landed in the audit table. Fired for all three input surfaces (banner, hosted-policy, API).
    compliance.alertA monitored regulatory change may impact your policies (Pro+ tier).
    Delivery envelope + headers
    // Every webhook delivery has this envelope:
    {
      "event": "consent.recorded",
      "timestamp": "2026-05-08T22:25:59.637Z",
      "data": {
        // event-specific payload — see Available events below
      }
    }
    
    // Headers we send:
    //   Content-Type: application/json
    //   User-Agent: PolicyForge-Webhook/2.0
    //   X-PolicyForge-Event: consent.recorded
    //   X-PolicyForge-Delivery: <uuid> (idempotency key)
    //   X-PolicyForge-Signature: sha256=<hmac>

    Verifying the signature

    We sign each payload with HMAC-SHA256 using your endpoint's secret (visible in the dashboard once on creation). Compare against the X-PolicyForge-Signature header in constant time before processing.

    import { createHmac, timingSafeEqual } from "node:crypto";
    
    export function verifySignature(req: Request, secret: string) {
      const signature = req.headers.get("x-policyforge-signature");
      if (!signature) return false;
    
      const body = await req.text(); // raw body, not parsed JSON
      const expected =
        "sha256=" +
        createHmac("sha256", secret).update(body).digest("hex");
    
      // Constant-time compare
      const sigBuf = Buffer.from(signature);
      const expBuf = Buffer.from(expected);
      return sigBuf.length === expBuf.length && timingSafeEqual(sigBuf, expBuf);
    }
    Retries. Failed deliveries (non-2xx response or timeout) are retried up to 3 times with exponential backoff (1m, 4m, 16m). After three failures the delivery is marked failed and surfaced in your dashboard's webhook log. Idempotent handlers should key on X-PolicyForge-Delivery so retries don't create duplicates downstream.

    Stuck or missing something?

    Email support@policyforge.co with the request you're making and the response you're getting — we'll respond within one business day.