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.

    Working in Claude Code, Cursor, or Windsurf? Skip the raw API — the PolicyForge MCP server exposes all of these endpoints as agent tools your AI editor calls directly.

    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. One of saas, e-commerce, mobile_app, fintech, financial, healthcare, education, real-estate, non-profit, consulting, media, other.
    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.
    service_descriptionstringFree-text description of what your product/service does and who uses it. The single highest-impact input for policy quality — be specific.
    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.
    data_retentionstringHow long you keep personal data — e.g. "Account data for the life of the account; analytics for 24 months."
    target_audiencestring[]Who uses the product — e.g. ["consumers", "students", "businesses"]. Shapes audience-specific language.
    governing_lawstringGoverning law / venue for the document — e.g. "State of Delaware, USA".
    sells_databooleanDo you sell or share personal data? false produces an explicit "we never sell your data" clause; true triggers opt-out language.
    security_measuresstring[]Safeguards you apply — e.g. ["encryption at rest", "TLS in transit", "SOC 2"].
    physical_addressstringRegistered business address, surfaced in the contact section.
    dpo_emailstringData Protection Officer contact. Providing it marks a DPO as appointed (GDPR-relevant).
    hosting_enabledboolean
    default: true
    Host the policy at a public policyforge.co URL (returned as hosted_url). Set false to keep it API-only.
    consent_trackingboolean
    default: false
    Show an "I agree" consent banner on the hosted page and record visitor consent (consent_records). Off by default.
    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
      }'
    Response201 Created
    {
      "id": "8331bf67-3748-471f-b595-9ba55a972cd5",
      "type": "privacy",
      "policy_type": "privacy",
      "title": "Privacy Policy - Acme Inc",
      "content": "...",
      "created_at": "2026-05-08T22:30:00.000+00:00",
      "status": "published",
      "hosting_enabled": true,
      "hosted_url": "https://policyforge.co/policy/privacy-acme-inc",
      "url_slug": "privacy-acme-inc"
    }
    Response400 Bad Request
    {
      "error": "Missing required field: jurisdiction"
    }
    Response402 Quota Exhausted
    {
      "error": "Monthly policy quota exhausted. Upgrade to continue.",
      "details": {
        "tier": "free",
        "reason": "Monthly limit reached (2/month). Upgrade to Pro for unlimited access.",
        "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
    {
      "policies": [
        {
          "id": "8331bf67-3748-471f-b595-9ba55a972cd5",
          "type": "privacy",
          "policy_type": "privacy",
          "title": "Privacy Policy - Acme Inc",
          "status": "published",
          "created_at": "2026-05-08T22:30:00.000+00:00",
          "updated_at": "2026-05-08T22:30:00.000+00:00"
        }
      ],
      "total": 1,
      "limit": 20,
      "offset": 0,
      "subscription_info": {
        "tier": "pro",
        "can_create_more": true,
        "remaining_policies": 999
      }
    }
    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",
      "type": "privacy",
      "policy_type": "privacy",
      "title": "Privacy Policy - Acme Inc",
      "content": "...",
      "status": "published",
      "hosting_enabled": true,
      "hosted_url": "https://policyforge.co/policy/privacy-acme-inc",
      "url_slug": "privacy-acme-inc",
      "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"
    }
    POST/api/v1/policies/{id}/regenerate

    Re-run the AI engine over the stored generation context merged with any changed fields.

    Same policy ID and hosted URL — published links keep working. The previous content is snapshotted to the version history first, and the response reports previous_version_saved. Accepts the same optional context fields as generate (plus company_name, contact_email, jurisdiction, business_type, stack_manifest); only the fields you send change — everything else keeps its stored value. Consumes one generation from your quota (same 402 contract as generate).

    Headers

    FieldTypeDescription
    AuthorizationrequiredstringBearer pf_…

    Path parameters

    FieldTypeDescription
    idrequireduuidPolicy UUID.
    Request
    curl -X POST https://policyforge.co/api/v1/policies/$POLICY_ID/regenerate \
      -H "Authorization: Bearer $POLICYFORGE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "payments": true,
        "third_party_integrations": ["PostHog", "Stripe"]
      }'
    Response200 Regenerated
    {
      "id": "8331bf67-3748-471f-b595-9ba55a972cd5",
      "type": "privacy",
      "title": "Privacy Policy - Acme Inc",
      "content": "# Privacy Policy…",
      "status": "published",
      "hosted_url": "https://policyforge.co/policy/privacy-acme-inc",
      "previous_version_saved": true,
      "subscription_info": {
        "tier": "pro",
        "remaining_policies": 998
      }
    }
    GET/api/v1/policies/{id}/versions

    Version history: snapshots taken before every update, regeneration, or restore.

    Metadata only — fetch /api/v1/policies/{id}/versions/{n} for a version's full content. To restore a version, PATCH the policy with that content; the outgoing content is snapshotted automatically, so restores are themselves reversible.

    Headers

    FieldTypeDescription
    AuthorizationrequiredstringBearer pf_…

    Path parameters

    FieldTypeDescription
    idrequireduuidPolicy UUID.
    Request
    curl https://policyforge.co/api/v1/policies/$POLICY_ID/versions \
      -H "Authorization: Bearer $POLICYFORGE_API_KEY"
    Response200 OK
    {
      "policy_id": "8331bf67-3748-471f-b595-9ba55a972cd5",
      "versions": [
        {
          "version_number": 2,
          "title": "Privacy Policy - Acme Inc",
          "change_summary": "Snapshot before regeneration via API",
          "is_auto_generated": false,
          "review_status": "snapshot",
          "created_at": "2026-07-13T13:26:15Z"
        }
      ]
    }
    POST/api/v1/audit

    Compliance gap analysis: compare a detected stack manifest against a policy.

    Send a manifest describing what your stack actually does (SDKs, data flows, cookies, jurisdictions — AI coding agents build this by scanning the codebase) and optionally a policy_id. Returns missing disclosures with severity and evidence, inaccurate or overstated claims, missing policy types, and recommendations. Without policy_id the manifest is audited on its own. Does not consume generation quota.

    Headers

    FieldTypeDescription
    AuthorizationrequiredstringBearer pf_…
    Request
    curl -X POST https://policyforge.co/api/v1/audit \
      -H "Authorization: Bearer $POLICYFORGE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "policy_id": "8331bf67-3748-471f-b595-9ba55a972cd5",
        "manifest": {
          "analytics": ["posthog"],
          "payments": ["stripe"],
          "ai_features": ["openai"],
          "jurisdiction_signals": ["EU"]
        }
      }'
    Response200 OK
    {
      "audit": {
        "summary": "The policy discloses Stripe and PostHog but omits the OpenAI integration…",
        "missing_disclosures": [
          {
            "finding": "Missing disclosure of OpenAI integration and AI features.",
            "manifest_evidence": "ai_features: openai",
            "why_required": "GDPR requires disclosing processors that receive user content.",
            "severity": "high"
          }
        ],
        "inaccurate_or_overstated": [],
        "missing_policies": [
          {
            "type": "cookie_policy",
            "why": "Analytics cookies are set."
          }
        ],
        "recommendations": [
          "Add an AI-processing section naming OpenAI…"
        ]
      },
      "audited_policy_id": "8331bf67-3748-471f-b595-9ba55a972cd5",
      "model": "google/gemini-3-pro-preview"
    }
    GET/api/v1/usage

    Check your subscription tier and remaining policy-generation quota.

    Read-only view of the same quota check the generate endpoint enforces — call it before POST /api/v1/policies to plan instead of handling 402s. On paid tiers unlimited is true and the counters are null; on the free tier reason explains why can_generate is false (monthly limit, daily limit, or 24-hour cooldown).

    Headers

    FieldTypeDescription
    AuthorizationrequiredstringBearer pf_…
    Request
    curl https://policyforge.co/api/v1/usage \
      -H "Authorization: Bearer $POLICYFORGE_API_KEY"
    Response200 Paid tier
    {
      "tier": "pro",
      "unlimited": true,
      "can_generate": true,
      "remaining": null,
      "limit": null,
      "used": null,
      "resets_at": null,
      "reason": null
    }
    Response200 Free tier (limit reached)
    {
      "tier": "free",
      "unlimited": false,
      "can_generate": false,
      "remaining": 0,
      "limit": 2,
      "used": 2,
      "resets_at": "2026-08-01",
      "reason": "Monthly limit reached (2/month). Upgrade to Pro for unlimited access."
    }

    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.