> ## Documentation Index
> Fetch the complete documentation index at: https://docs.percify.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Call the Percify API from Node.js, Python and cURL

> Percify has no SDK package: call the REST API with fetch in Node.js, requests in Python or cURL. Copyable run, wait, estimate and error handling code.

Percify does not publish an SDK, so there is no npm or PyPI package to install. The REST API is small enough to call directly: `POST /v1/run` starts a job and `GET /v1/generations/{id}?wait=45` returns the finished file. The code below uses the built-in `fetch` in Node.js 18 or newer, `requests` in Python, and plain cURL.

Before you start, create a key on the [developer page](https://app.percify.io/home/developer) (Scale and Ultra plans) and export it:

```bash theme={"system"}
export PERCIFY_API_TOKEN="pk_live_…"
```

## Generate a file and wait for it

<CodeGroup>
  ```javascript Node.js theme={"system"}
  // percify.mjs  (Node.js 18+, no dependencies)
  const API = "https://api.percify.io/v3/playground/v1";
  const headers = {
    Authorization: `Bearer ${process.env.PERCIFY_API_TOKEN}`,
    "Content-Type": "application/json",
  };

  async function call(method, path, body, extraHeaders = {}) {
    const res = await fetch(`${API}${path}`, {
      method,
      headers: { ...headers, ...extraHeaders },
      body: body ? JSON.stringify(body) : undefined,
    });
    const json = await res.json().catch(() => null);
    if (!res.ok) {
      // Most errors are { statusCode, message, error }; 429s are a plain string.
      const message = typeof json === "string" ? json : json?.message;
      throw new Error(`Percify ${res.status}: ${message}`);
    }
    return json.data;
  }

  export async function generate(modelId, input, { idempotencyKey } = {}) {
    const extra = idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {};
    let gen = await call("POST", "/run", { modelId, input }, extra);
    while (gen.status !== "succeeded" && gen.status !== "failed") {
      gen = await call("GET", `/generations/${gen.id}?wait=45`);
    }
    if (gen.status === "failed") throw new Error(`Generation failed: ${gen.error}`);
    return gen; // gen.output.urls, gen.creditsSpent
  }

  const quote = await call("POST", "/estimate", {
    modelId: "gpt-image-2",
    input: { prompt: "Flat illustration of a paper plane", quality: "low" },
  });
  console.log(`This will cost ${quote.credits} credits`);

  const gen = await generate(
    "gpt-image-2",
    { prompt: "Flat illustration of a paper plane", quality: "low" },
    { idempotencyKey: "paper-plane-001" },
  );
  console.log(gen.output.urls);
  ```

  ```python Python theme={"system"}
  # percify.py  (pip install requests)
  import os
  import requests

  API = "https://api.percify.io/v3/playground/v1"
  SESSION = requests.Session()
  SESSION.headers["Authorization"] = f"Bearer {os.environ['PERCIFY_API_TOKEN']}"


  def call(method, path, body=None, headers=None):
      res = SESSION.request(method, f"{API}{path}", json=body, headers=headers, timeout=70)
      try:
          payload = res.json()
      except ValueError:
          payload = None
      if not res.ok:
          # Most errors are {statusCode, message, error}; 429s are a plain string.
          message = payload if isinstance(payload, str) else (payload or {}).get("message")
          raise RuntimeError(f"Percify {res.status_code}: {message}")
      return payload["data"]


  def generate(model_id, input, idempotency_key=None):
      headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
      gen = call("POST", "/run", {"modelId": model_id, "input": input}, headers)
      while gen["status"] not in ("succeeded", "failed"):
          gen = call("GET", f"/generations/{gen['id']}?wait=45")
      if gen["status"] == "failed":
          raise RuntimeError(f"Generation failed: {gen['error']}")
      return gen  # gen["output"]["urls"], gen["creditsSpent"]


  if __name__ == "__main__":
      inputs = {"prompt": "Flat illustration of a paper plane", "quality": "low"}
      quote = call("POST", "/estimate", {"modelId": "gpt-image-2", "input": inputs})
      print(f"This will cost {quote['credits']} credits")
      gen = generate("gpt-image-2", inputs, idempotency_key="paper-plane-001")
      print(gen["output"]["urls"])
  ```

  ```bash cURL theme={"system"}
  #!/usr/bin/env bash
  # Needs curl and jq.
  set -euo pipefail
  API="https://api.percify.io/v3/playground/v1"
  AUTH="Authorization: Bearer $PERCIFY_API_TOKEN"
  BODY='{"modelId":"gpt-image-2","input":{"prompt":"Flat illustration of a paper plane","quality":"low"}}'

  # 1. Price it (no charge)
  curl -s -X POST "$API/estimate" -H "$AUTH" -H "Content-Type: application/json" -d "$BODY" | jq .data

  # 2. Start it
  GEN_ID=$(curl -s -X POST "$API/run" -H "$AUTH" -H "Content-Type: application/json" \
    -H "Idempotency-Key: paper-plane-001" -d "$BODY" | jq -r .data.id)

  # 3. Wait for it
  while :; do
    GEN=$(curl -s "$API/generations/$GEN_ID?wait=45" -H "$AUTH")
    STATUS=$(echo "$GEN" | jq -r .data.status)
    if [ "$STATUS" = "succeeded" ] || [ "$STATUS" = "failed" ]; then break; fi
  done
  echo "$GEN" | jq .data
  ```
</CodeGroup>

## Find a model's inputs

Each model has its own `input` fields. Read them from the public catalog, no key needed:

<CodeGroup>
  ```javascript Node.js theme={"system"}
  const res = await fetch("https://api.percify.io/v3/playground/v1/models/infinitetalk-fast");
  const { data } = await res.json();
  console.log(data.input_schema.required, Object.keys(data.input_schema.properties));
  ```

  ```python Python theme={"system"}
  import requests
  data = requests.get("https://api.percify.io/v3/playground/v1/models/infinitetalk-fast").json()["data"]
  print(data["input_schema"]["required"], list(data["input_schema"]["properties"]))
  ```

  ```bash cURL theme={"system"}
  curl -s https://api.percify.io/v3/playground/v1/models/infinitetalk-fast | jq '.data.input_schema'
  ```
</CodeGroup>

## Handle errors

| Status | What to do in code                                                                                                              |
| ------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Fix the request. The `message` names the field or problem, for example `Missing required field: audio` or `Not enough credits`. |
| `401`  | Check the key. It is missing, wrong, expired or revoked.                                                                        |
| `404`  | The generation or model id does not exist for your account.                                                                     |
| `429`  | Wait about a minute before retrying. The body is a JSON string such as `"API key rate limit exceeded ..."`.                     |
| `503`  | The model is busy and your credits were refunded. Retry with backoff.                                                           |

See [Errors and rate limits](/api-reference/errors-and-limits) for every message.

## Good habits

* Keep the key on your server. Browsers on other websites cannot call the API, and client code exposes the key.
* Send an `Idempotency-Key` on every `POST /v1/run` that might be retried.
* Download the files in `output.urls` to your own storage when you need to keep them.
* For jobs that take minutes, pass a [webhook](/guides/webhooks) and skip the loop.

## Related

<CardGroup cols={2}>
  <Card title="Make a talking avatar video" href="/api-reference/avatars/overview">
    A three-step pipeline built on these helpers.
  </Card>

  <Card title="Start a generation" href="/api-reference/generations/run">
    Every field of POST /v1/run.
  </Card>

  <Card title="Authentication" href="/percify/api-auth">
    Create, cap and revoke keys.
  </Card>

  <Card title="MCP server" href="/mcp-server">
    Let an AI agent call Percify for you.
  </Card>
</CardGroup>
