API Reference
← Back to home Dashboard Get API Key
Documentation

PlanPlain API Reference

PlanPlain accepts Terraform plan JSON and returns a plain-English summary, risk flags, reviewer checklist, and PR-ready markdown — in one API call.

Base URL: https://planplain.polsia.app

Quick Start

Three steps from terraform plan to a PR comment.

1
Get an API key

Generate a free API key with your email. No account signup required.

bash
curl -X POST https://planplain.polsia.app/api/keys \
  -H "Content-Type: application/json" \
  -d '{"email": "you@yourcompany.com"}'
response
{
  "success": true,
  "api_key": "pp_live_a1b2c3d4e5f6..."  // save this — shown once
}
2
Export your Terraform plan as JSON

Run these commands in your Terraform directory to get the plan JSON.

bash
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
3
Analyze the plan

POST the JSON file to /api/analyze. Get back a structured analysis you can post as a PR comment.

bash
curl -X POST https://planplain.polsia.app/api/analyze \
  -H "Authorization: Bearer pp_live_a1b2c3d4e5f6..." \
  -H "Content-Type: application/json" \
  -d @plan.json

The response includes pr_markdown — paste it directly into your PR comment or automate via GitHub Actions.

Check your usage

See requests, error rate, response times, and rate limit status in real time.

View your dashboard →


Authentication

All requests to /api/analyze require a Bearer token in the Authorization header.

http header
Authorization: Bearer pp_live_<your_key>
Property Value
Format pp_live_ prefix + 48 hex characters
Storage SHA-256 hashed server-side — we never store your raw key
Scope Full access to /api/analyze
Max per email 5 API keys
⚠️
Keep your key secret. It's shown once at creation time. If you lose it, generate a new one — old keys stay active.

Rate Limits

Rate limits are per API key using a sliding window.

Window
Limit
Scope
1 min
rolling window
10
requests
per API key
24 hrs
rolling window
200
requests
per API key

When rate limited, the API returns HTTP 429 with a Retry-After header (seconds to wait).

Rate limit headers

response headers
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
Retry-After: 42   # only present when 429

POST /api/analyze

Analyzes a Terraform plan JSON and returns a plain-English breakdown with risk assessment.

POST /api/analyze 🔑 Auth required

Accepts the output of terraform show -json tfplan. Returns structured analysis + PR markdown.

Request body

Pass the raw Terraform plan JSON as the request body. This is the complete JSON output from terraform show -json.

FieldTypeRequiredDescription
plan_json object optional Nested under a key. Or send the raw plan JSON as the body directly.
resource_changes array required Array of resource change objects. This field must exist in the parsed JSON.
💡
Tip: Send the entire file as the request body — no wrapping needed. The plan JSON from terraform show -json is accepted directly.

Response body

FieldTypeDescription
success boolean true on success
analysis.summary string Plain-English description of what this plan does
analysis.risk_flags array Array of { severity, resource, message } objects. Severity: HIGH, MED, LOW
analysis.reviewer_checklist array String array of things a reviewer should verify
analysis.pr_markdown string Formatted markdown ready to post as a PR comment
metadata object Resource counts, max risk level, processing time
200 OK
{
  "success": true,
  "analysis": {
    "summary": "This plan creates 2 resources and destroys 1. The main risk is a database deletion (aws_db_instance.main) which cannot be undone.",
    "risk_flags": [
      {
        "severity": "HIGH",
        "resource": "aws_db_instance.main",
        "message": "Database deletion — data will be permanently lost"
      }
    ],
    "reviewer_checklist": [
      "Confirm database backup was taken before merging",
      "Verify deletion protection is intentionally disabled"
    ],
    "pr_markdown": "## 🔍 Terraform Plan Summary\n\n**2 to add** | **0 to change** | **1 to destroy**\n\n..."
  },
  "metadata": {
    "resources_total": 3,
    "resources_created": 2,
    "resources_updated": 0,
    "resources_destroyed": 1,
    "max_risk_level": "HIGH",
    "processing_time_ms": 312
  }
}

POST /api/keys

Generate an API key. No authentication required.

POST /api/keys No auth

Creates a new API key tied to an email address. Returns the key once — store it immediately.

FieldTypeRequiredDescription
email string required Your email address. Used to group keys and enforce the 5-key limit.
name string optional A label for this key, e.g. "ci-prod" or "local-dev"

GET /api/keys/verify

Verify an API key is valid and active. Useful for CI pipeline health checks.

GET /api/keys/verify 🔑 Auth required

Returns key metadata including prefix and creation date.


GET /api/example

Returns a sample Terraform plan JSON for testing. No authentication required.

GET /api/example No auth

Good for testing your integration without a real Terraform plan.

bash
curl https://planplain.polsia.app/api/example | \
  curl -X POST https://planplain.polsia.app/api/analyze \
    -H "Authorization: Bearer pp_live_..." \
    -H "Content-Type: application/json" \
    -d @-

Code Examples

Full pipeline — bash script

bash
#!/usr/bin/env bash
# planplain-analyze.sh — analyze a terraform plan and print the summary

API_KEY="pp_live_your_key_here"
PLAN_FILE="plan.json"

# Generate plan JSON
terraform plan -out=tfplan
terraform show -json tfplan > "$PLAN_FILE"

# Send to PlanPlain
RESPONSE=$(curl -s -X POST https://planplain.polsia.app/api/analyze \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d @"$PLAN_FILE")

# Print summary
echo "$RESPONSE" | jq -r '.analysis.summary'

# Print risk flags
echo "$RESPONSE" | jq -r '.analysis.risk_flags[] | "[\(.severity)] \(.resource): \(.message)"'

# Print PR markdown
echo "$RESPONSE" | jq -r '.analysis.pr_markdown'

Node.js / TypeScript

javascript
// analyze-plan.js — Node.js 18+ (native fetch)
const fs = require('fs');

async function analyzePlan(planFilePath) {
  const planJson = JSON.parse(fs.readFileSync(planFilePath, 'utf8'));

  const response = await fetch('https://planplain.polsia.app/api/analyze', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.PLANPLAIN_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(planJson),
  });

  if (!response.ok) {
    const err = await response.json();
    throw new Error(`PlanPlain error: ${err.message}`);
  }

  const { analysis, metadata } = await response.json();

  console.log('Summary:', analysis.summary);
  console.log('Risk level:', metadata.max_risk_level);
  console.log('Risk flags:', analysis.risk_flags.length);

  // Post pr_markdown as a GitHub PR comment (example)
  return analysis.pr_markdown;
}

analyzePlan('./plan.json')
  .then(markdown => console.log(markdown))
  .catch(console.error);

TypeScript (with types)

typescript
interface RiskFlag {
  severity: 'HIGH' | 'MED' | 'LOW';
  resource: string;
  message: string;
}

interface PlanPlainResponse {
  success: boolean;
  analysis: {
    summary: string;
    risk_flags: RiskFlag[];
    reviewer_checklist: string[];
    pr_markdown: string;
  };
  metadata: {
    resources_total: number;
    resources_created: number;
    resources_updated: number;
    resources_destroyed: number;
    max_risk_level: string;
    processing_time_ms: number;
  };
}

async function analyzePlan(planJson: object): Promise<PlanPlainResponse> {
  const res = await fetch('https://planplain.polsia.app/api/analyze', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.PLANPLAIN_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(planJson),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<PlanPlainResponse>;
}

Python

python
# analyze_plan.py — Python 3.7+ (no extra dependencies)
import json
import os
import urllib.request

def analyze_plan(plan_file_path: str) -> dict:
    # Load the plan JSON
    with open(plan_file_path) as f:
        plan_json = json.load(f)

    api_key = os.environ["PLANPLAIN_API_KEY"]
    url = "https://planplain.polsia.app/api/analyze"

    req = urllib.request.Request(
        url,
        data=json.dumps(plan_json).encode(),
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
        method="POST",
    )

    with urllib.request.urlopen(req) as response:
        result = json.loads(response.read())

    return result

# Usage
result = analyze_plan("plan.json")
analysis = result["analysis"]

print(f"Summary: {analysis['summary']}")
print(f"Risk flags: {len(analysis['risk_flags'])}")

for flag in analysis["risk_flags"]:
    print(f"  [{flag['severity']}] {flag['resource']}: {flag['message']}")

print("\nPR Markdown:")
print(analysis["pr_markdown"])

With requests library

python
import requests
import json, os

with open("plan.json") as f:
    plan = json.load(f)

response = requests.post(
    "https://planplain.polsia.app/api/analyze",
    headers={"Authorization": f"Bearer {os.environ['PLANPLAIN_API_KEY']}"},
    json=plan,
    timeout=60,
)
response.raise_for_status()

data = response.json()
print(data["analysis"]["pr_markdown"])

GitHub Actions Integration

Use the official PlanPlain GitHub Action to automatically comment on PRs whenever Terraform files change.

.github/workflows/terraform-review.yml
name: Terraform Plan Review
on:
  pull_request:
    paths: ['**.tf', '**.tfvars']

permissions:
  pull-requests: write

jobs:
  terraform-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_wrapper: false

      - name: Generate plan JSON
        run: |
          terraform init
          terraform plan -out=tfplan
          terraform show -json tfplan > plan.json
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: Analyze with PlanPlain
        uses: Polsia-Inc/planplain/github-action@main
        with:
          plan_file: plan.json
          api_key: ${{ secrets.PLANPLAIN_API_KEY }}
          github_token: ${{ secrets.GITHUB_TOKEN }}
          post_comment: true
🔑
Add your API key as a GitHub secret: Settings → Secrets → Actions → New secret, name it PLANPLAIN_API_KEY.

Error Codes

All errors follow the same format: { "success": false, "error": "CODE", "message": "description" }

HTTPError CodeDescription
400 INVALID_PLAN Request body is not valid Terraform plan JSON. Missing resource_changes.
401 MISSING_API_KEY No Authorization header provided.
401 INVALID_API_KEY Key not found or has been deactivated.
429 RATE_LIMITED Too many requests. Check Retry-After header for wait time (seconds).
500 ANALYSIS_FAILED Internal error during analysis. Heuristic fallback still runs — partial results may be available.
429 Too Many Requests
{
  "success": false,
  "error": "RATE_LIMITED",
  "message": "Rate limit exceeded. Try again in 42 seconds.",
  "retry_after": 42
}
PlanPlain · API v1.0
Questions? hello@planplain.polsia.app