Clone a Website With an API — Built for Agents
A developer's guide to the Clonesite clone API. Create one key, then let an agent create clone requests, poll status, handle webhooks, and download editable React and Tailwind source — end to end.

Most APIs assume a human will read the docs, wire up the calls, and babysit the integration. This one doesn't.
With the Clonesite clone API, a person does exactly one thing — create a key — and an agent does everything else: it preflights the request, runs a free mock integration test, turns a live URL into editable React and Tailwind source, waits for the build, and downloads the code. You make the key; it does the rest.

One human step, then a loop the agent owns: check, create, wait, download.
The mental model
The integration has one human action and five API endpoints:
- Human (once): create an API key on
/developers. - Agent (before spending):
POST /clone-requests/preflightand, during setup,POST /clone-requests/test-runs. - Agent (live clone):
POSTa clone request, poll the status (or receive a webhook), thenPOSTto unlock and download the source.
That's it. No SDK is required, no browser session, no dashboard clicking after the key exists. The base URL is:
https://clonesite.ai/api/v1
Step 1 — Create a key (the only human step)
Sign in, open /developers, and click Create your first
key. You'll see a value like cs_live_a1b2c3d4... exactly once. It's stored hashed, so copy it
immediately and hand it to your agent (or drop it in an environment variable).
Treat the key like a password: anyone holding it can spend your credits. The developers page even gives you a ready-to-paste agent brief:
# Clone any site via the Clonesite API
base https://clonesite.ai/api/v1
auth x-api-key: cs_live_...
spec https://clonesite.ai/openapi.json
discovery https://clonesite.ai/llms.txt
Agents can't self-register or mint their own credentials. A human account owner always creates the key first — then an agent uses it.
Step 2 — Authenticate every call
Every request carries the key in the x-api-key header. There is no OAuth dance and no bearer
token exchange:
curl https://clonesite.ai/api/v1/clone-requests/api_req_example \
-H "x-api-key: cs_live_a1b2c3d4..."
Each key is scoped to explicit permissions: clone_requests:create, clone_requests:read, and
source_downloads:create. Preflight and test-runs use the create permission, status polling uses
read, and source ZIP downloads use the source-download permission.
Step 3 — Preflight and run a free test

Preflight checks before you charge. Test runs exercise polling, webhooks, and downloads for free.
Before spending credits, validate the same payload with preflight. It checks the key, permission,
payload, credits, and webhook configuration, but does not write a request, create a job, charge
credits, or send a webhook.
curl -X POST https://clonesite.ai/api/v1/clone-requests/preflight \
-H "x-api-key: cs_live_a1b2c3d4..." \
-H "Content-Type: application/json" \
-d '{
"sourceUrl": "https://stripe.com",
"prompt": "Clone this site as an editable React and Tailwind app.",
"externalRequestId": "order_123",
"callbackUrl": "https://your-app.com/webhooks/clonesite"
}'
A passing preflight confirms the call will work — without creating anything or moving credits:
{
"ok": true,
"mode": "preflight",
"canCreate": true,
"wouldChargeCredits": 5,
"wouldCreateCloneRequest": false,
"wouldCreateJob": false,
"checks": {
"apiKey": "valid",
"permission": "clone_requests:create",
"payload": "valid",
"credits": "sufficient",
"webhook": "configured"
}
}
During setup, run test-runs with the same live key and webhook handler. It creates a free
mode: "test" request and sends signed clone.ready or clone.failed webhooks. The response is
the same request object a live call returns — not a download URL — so you exercise polling,
webhooks, and even a free fixture source download through the exact same endpoints, without paying
for a real clone.
curl -X POST https://clonesite.ai/api/v1/clone-requests/test-runs \
-H "x-api-key: cs_live_a1b2c3d4..." \
-H "Idempotency-Key: test_order_123" \
-H "Content-Type: application/json" \
-d '{
"sourceUrl": "https://stripe.com",
"prompt": "Test my Clone API integration.",
"externalRequestId": "test_order_123",
"callbackUrl": "https://your-app.com/webhooks/clonesite",
"scenario": "ready"
}'
The test run returns mode: "test". Poll it or wait for the webhook exactly like a live request — it
reaches ready (or failed, per scenario) without spending a credit:
{
"id": "api_req_def456",
"mode": "test",
"status": "queued",
"sourceZip": { "available": false, "unlocked": false, "creditCost": 0 },
"statusUrl": "/api/v1/clone-requests/api_req_def456"
}
Step 4 — Create a live clone request
Send the public URL you want to clone and a prompt describing the output. The Idempotency-Key
header is required on live create and test-runs — it's what makes retries safe.
curl -X POST https://clonesite.ai/api/v1/clone-requests \
-H "x-api-key: cs_live_a1b2c3d4..." \
-H "Idempotency-Key: order_123" \
-H "Content-Type: application/json" \
-d '{
"sourceUrl": "https://stripe.com",
"prompt": "Clone this site as an editable React and Tailwind app.",
"externalRequestId": "order_123",
"callbackUrl": "https://your-app.com/webhooks/clonesite"
}'
externalRequestId and callbackUrl are optional. You get back 202 Accepted and a stable
api_req_* id:
{
"id": "api_req_abc123",
"mode": "live",
"status": "queued",
"sourceUrl": "https://stripe.com/",
"credits": { "charged": true, "creditCost": 5, "refunded": false },
"sourceZip": { "available": false, "unlocked": false, "creditCost": 100 },
"statusUrl": "/api/v1/clone-requests/api_req_abc123"
}
Creating a live request costs 5 credits, charged immediately. Preview is free; downloading source is a separate, later action (Step 6).
Idempotency, the right way
The Idempotency-Key is your safety net for flaky networks:
- Same key + same body → you get the same request back. Retry as often as you like; you're never double-charged and never get a duplicate job.
- Same key + different body →
409 idempotency_conflict. The key is bound to the first payload it saw (including thecallbackUrl).
Use something stable and meaningful, like order:${orderId} or a UUID you persist next to the work.
Step 5 — Wait for ready (poll or webhook)
A clone runs asynchronously and usually takes about 1–3 hours. There are two ways to learn when it's done.
Poll the status endpoint:
curl https://clonesite.ai/api/v1/clone-requests/api_req_abc123 \
-H "x-api-key: cs_live_a1b2c3d4..."
The status moves through a small, predictable lifecycle, and the credit accounting is built in:

Five credits on create. Failed clones refund automatically. Preview stays free.
A ready response exposes the preview and whether source is available — but never a download URL:
{
"id": "api_req_abc123",
"mode": "live",
"status": "ready",
"previewUrl": "https://preview.clonesite.ai/...",
"sourceZip": { "available": true, "unlocked": false, "creditCost": 100 }
}
If the clone fails, the status becomes failed and the 5 credits are refunded automatically —
you don't pay for builds that don't land.
Or receive a webhook. If you passed a callbackUrl, Clonesite POSTs a signed event the moment
the request reaches a terminal state:
event: clone.ready | clone.failed
{
"event": "clone.ready",
"id": "evt_abc123",
"mode": "live",
"cloneRequestId": "api_req_abc123",
"templateSlug": "stripe-a1b2c3",
"previewUrl": "https://preview.clonesite.ai/...",
"sourceZip": { "available": true, "unlocked": false, "creditCost": 100 }
}
Verify the signature before trusting the body. Each delivery includes a timestamp and an HMAC over
timestamp + "." + rawBody:
X-Clonesite-Timestamp: 1781946000
X-Clonesite-Signature: v1=<hex hmac_sha256>
import crypto from "node:crypto";
function verifyClonesiteWebhook(rawBody, headers, secret) {
const ts = headers["x-clonesite-timestamp"];
const signature = headers["x-clonesite-signature"]; // "v1=<hex>"
const expected =
"v1=" + crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Webhooks are a notification, not the source of truth. Deliveries retry with backoff, and the body never carries the download URL — so even a leaked webhook log can't be used to pull your source. After receiving one, call the status endpoint to confirm.
Step 6 — Download the source (optional)
The source ZIP is a deliberate second step, taken after a human (or your agent) is happy with the preview. Unlocking it the first time costs 100 credits:
curl -X POST https://clonesite.ai/api/v1/clone-requests/api_req_abc123/source-downloads \
-H "x-api-key: cs_live_a1b2c3d4..."
{
"downloadUrl": "https://r2.clonesite.ai/signed-url",
"expiresAt": 1781949600000,
"filename": "stripe-a1b2c3-source.zip",
"creditCost": 100,
"alreadyUnlocked": false,
"artifact": {
"artifactId": "src_art_abc123",
"templateSlug": "stripe-a1b2c3",
"filename": "stripe-a1b2c3-source.zip",
"contentType": "application/zip",
"status": "ready",
"checksumSha256": "9f86d081884c7d65...",
"sizeBytes": 5242880,
"createdAt": 1781949000000,
"updatedAt": 1781949600000
}
}
The URL is short-lived (about 5 minutes) — download or copy the ZIP to your own storage before expiresAt. Calling
the endpoint again only re-signs a fresh URL; it does not charge another 100 credits once the
request is unlocked.
Errors worth handling
The API returns a stable JSON error shape, so your agent can branch on error.code instead of
parsing prose:
{ "error": { "code": "insufficient_credits", "message": "..." } }
400—invalid_request,missing_idempotency_key, orinvalid_json: a bad body, a missingIdempotency-Keyheader, or unparseable JSON.401—missing_api_keyorinvalid_api_key: no key, or a wrong or revoked key.402—insufficient_credits: not enough credits to create or unlock.403—insufficient_permissionsorapi_key_account_not_found: the key lacks the required permission, or its account is unavailable.404—not_found: unknown request — also returned for a request that belongs to another account.409—idempotency_conflict,request_not_ready, orsource_artifact_not_ready: a reused key with a new body, or source requested before the clone (or its artifact) is ready.422—invalid_callback_urlorwebhook_not_configured: a malformed callbackUrl, or one passed without active webhook settings.429—rate_limitedorusage_exceeded: slow down and honor theRetry-Afterheader.500—internal_error: an unexpected server error; retry with the sameIdempotency-Key.
That's every code the API emits — openapi.json carries the same enumeration, so an agent can map each one without reading this page.
A revoked key starts returning 401 immediately, so rotating credentials is instant.
Built for agents: discovery
Here's the part that makes this an agent API rather than just a REST API: an agent doesn't need a human to read these docs. Hand it the key and two URLs, and it discovers the rest itself.

Hand it one key and two links; it reads the spec and constructs the calls.
- llms.txt describes, in plain language, what Clonesite does, its limits, and how an agent should use it.
- openapi.json is the machine-readable contract: preflight,
free test-runs, live create, status polling, source downloads, the
x-api-keyscheme, and every error code.
Building an MCP server? Today the public API is REST +
openapi.json. There is no hosted/mcpendpoint yet, but the endpoints map cleanly onto MCP tools (preflight, test-run, create, get, source-download) — so you can wrap them as your own MCP server and hand that to an agent instead.
Putting it together
Here's the whole live loop in one place — preflight, create, poll, download — with native fetch
and no dependencies:
const BASE = "https://clonesite.ai/api/v1";
const KEY = process.env.CLONESITE_API_KEY;
async function cloneSite(sourceUrl, prompt, idempotencyKey) {
// 1. Preflight (no side effects)
const preflight = await api("/clone-requests/preflight", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sourceUrl, prompt }),
});
if (!preflight.canCreate)
throw new Error(`preflight blocked: ${JSON.stringify(preflight.checks)}`);
// 2. Create live request (spends 5 credits)
let req = await api("/clone-requests", {
method: "POST",
headers: { "Idempotency-Key": idempotencyKey, "Content-Type": "application/json" },
body: JSON.stringify({ sourceUrl, prompt }),
});
// 3. Poll until terminal
while (req.status !== "ready" && req.status !== "failed") {
await new Promise((r) => setTimeout(r, 5000));
req = await api(`/clone-requests/${req.id}`);
}
if (req.status === "failed") throw new Error("clone failed (credits refunded)");
// 4. Download source (spends 100 credits on first unlock)
const dl = await api(`/clone-requests/${req.id}/source-downloads`, { method: "POST" });
return dl.downloadUrl;
}
async function api(path, init = {}) {
const res = await fetch(`${BASE}${path}`, {
...init,
headers: { "x-api-key": KEY, ...(init.headers ?? {}) },
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error?.code}`);
return body;
}
That function is the entire integration. Give your agent a key, point it at this loop, and it can clone any public site into editable source on its own.
Create your key on /developers and let your agent take it from there.
Related guides
5 Design Moves That Turn a Cloned Page Into a BrandFive concrete design decisions — one accent color, a serif voice, white space, real proof, and design tokens — that turn a cloned page into a brand without looking copied.
Before and After: Redesigning Our Own Landing Page From Tool to BrandA section-by-section before and after of our own landing page redesign — one accent color, a serif voice, white space, real proof, and a workflow you can copy.
How to Clone a Website From a URLA practical guide to cloning a website from its URL, replacing the brand, and turning the result into your own editable source.