API Reference Overview
iContentForge exposes APIs for integrating content generation and publishing into your own workflows. This overview covers authentication, signature verification, rate limits, and error handling conventions that apply across all endpoints.
Base URL
https://icontentforge.com/api
The Public Sitemap API uses a dedicated path: /api/public/sitemap/:projectId — no authentication required.
Authentication
Bearer Token (Recommended)
Pass your API key in the Authorization header:
curl -X GET 'https://icontentforge.com/api/v1/projects' \
-H 'Authorization: Bearer YOUR_API_KEY'
Never expose API keys in client-side JavaScript. Rotate keys immediately if compromised. Manage your keys from Account Settings → API Keys.
Webhook Signature Verification
iContentForge signs all outgoing webhook requests with HMAC-SHA256. The signature is sent in the X-ICF-Signature HTTP header.
How It Works
- iContentForge computes
HMAC-SHA256(secret, rawRequestBody)and sends the hex digest inX-ICF-Signature. - Your server reads the raw request body bytes and computes the same HMAC using your shared Secret Key.
- Compare the two values using a timing-safe equality check.
Critical: You must compute the HMAC over the raw request body string — not a re-serialized version. If you call req.json() (which parses JSON) and then JSON.stringify() again, the byte sequence may differ (field ordering, whitespace), and the signatures will not match.
✅ Correct: rawBody = await req.text() → compute HMAC → then JSON.parse(rawBody)
❌ Wrong: body = await req.json() → JSON.stringify(body) → compute HMAC
Verification Example — Node.js / Next.js App Router
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
export async function POST(req: NextRequest) {
// ✅ Step 1: Read raw body BEFORE parsing
const rawBody = await req.text();
// ✅ Step 2: Read signature header
const signature =
req.headers.get("x-icf-signature")
req.headers.get("x-contentforge-signature") // legacy header
"";
// ✅ Step 3: Compute expected HMAC using the same raw bytes
const secret = process.env.ICF_WEBHOOK_SECRET!;
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody) // ← raw string, NOT JSON.stringify(parsed)
.digest("hex");
// ✅ Step 4: Timing-safe comparison
const valid = (() => {
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
} catch {
return false; // different lengths → invalid
}
})();
if (!valid) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
// ✅ Step 5: Safe to parse now
const payload = JSON.parse(rawBody);
console.log("Article received:", payload.title);
return NextResponse.json({ success: true });
}
Verification Example — Express.js
const express = require("express");
const crypto = require("crypto");
const app = express();
// ⚠️ Use express.raw() — NOT express.json() — to get the raw Buffer
app.use("/webhook/icf", express.raw({ type: "application/json" }));
app.post("/webhook/icf", (req, res) => {
const rawBody = req.body; // Buffer
const signature = req.headers["x-icf-signature"] "";
const expected = crypto
.createHmac("sha256", process.env.ICF_WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: "Invalid signature" });
}
const payload = JSON.parse(rawBody.toString());
res.json({ received: true });
});
Verification Example — Python / Flask
import hashlib, hmac, json, os
from flask import request, jsonify, abort
@app.route("/webhook/icf", methods=["POST"])
def handle_webhook():
secret = os.environ["ICF_WEBHOOK_SECRET"].encode()
signature = request.headers.get("X-ICF-Signature", "")
# ✅ get_data() returns raw bytes — do NOT use request.json
raw_body = request.get_data()
expected = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
payload = json.loads(raw_body)
return jsonify({"received": True}), 200
Available APIs
Generic Webhook (Push)
iContentForge pushes a signed POST request to your endpoint when an article is ready to publish. See the full guide: Generic Webhook Integration.
Public Sitemap API
Returns a dynamically generated XML sitemap for your published articles. No authentication required.
GET /api/public/sitemap/:projectId
# Example
curl https://icontentforge.com/api/public/sitemap/proj_abc123
Rate Limits
Public API rate limits may change as the product evolves. Treat 429 Too Many Requests as the source-of-truth response for temporary throttling and retry using exponential backoff.
Rate-limited responses include headers such as:
| Header | Description |
|---|---|
X-RateLimit-Limit | Your limit per minute. |
X-RateLimit-Remaining | Requests remaining in current window. |
X-RateLimit-Reset | Unix epoch time when the window resets. |
Standard Error Format
{
"error": "RESOURCE_NOT_FOUND",
"message": "The requested article could not be found.",
"details": { "pageId": "cma1b2c3d4e5f" }
}
| HTTP Code | Meaning |
|---|---|
400 | Bad Request — malformed JSON or invalid parameters. |
401 | Unauthorized — missing or invalid API key / webhook signature. |
404 | Not Found — project or article does not exist. |
429 | Too Many Requests — rate limit exceeded. |
500 | Internal Server Error — something went wrong on our side. |
Next Steps
- Set up your webhook receiver: Generic Webhook Integration
- See all template variables: Webhook Payload Variables
- Understand your content pipeline: Publishing Overview