Integration recipe

Publish to pixPLace from Node.js with fetch

A Node.js script with fetch that uploads media by URL and publishes a post, with safe retries via Idempotency-Key.

Who it is for

Web developers who want to publish from a Node.js script, a serverless function or a build step.

What it automates

Publish a post with an image from code: upload by URL first, then create the post.

Steps

  1. Use Node.js 18 or newer: fetch is built in. Keep the API key in the environment variable PIXPLACE_API_KEY, never in the source.
  2. Write one helper that adds the Authorization header and the Idempotency-Key, and retries on 429 and 503 after Retry-After.
  3. Upload with POST /api/v1/uploads/from-url, then create the post with POST /api/v1/posts using data.publicUrl.
  4. Call the API from a server only. A key in browser code is visible to every visitor.
  5. When you create the key, tick the `publish` scope. A key without it gets 403 insufficient_scope on these calls.

Example

Placeholders: ppk_live_XXXXXXXX is your key. The ids and the image address are samples. The API address is always https://pixplace.space/api/v1.

publish.mjs
const BASE = "https://pixplace.space/api/v1";
const API_KEY = process.env.PIXPLACE_API_KEY;
const TODAY = new Date().toISOString().slice(0, 10);

async function call(method, path, body, key) {
  const headers = { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" };
  if (key) headers["Idempotency-Key"] = key;
  for (let attempt = 0; attempt < 4; attempt++) {
    const res = await fetch(BASE + path, { method, headers, ...(body ? { body: JSON.stringify(body) } : {}) });
    if (res.status === 429 || res.status === 503) {
      const wait = Number(res.headers.get("retry-after") || 5);
      if (wait > 120) throw new Error(`limit reached, retry in ${wait} s`);
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue;
    }
    if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
    return res.json();
  }
  throw new Error("still limited after 4 tries");
}

const upload = (await call("POST", "/uploads/from-url", { url: "https://example.com/photos/living-room.jpg" }, `upload-${TODAY}`)).data;
const post = await call(
  "POST",
  "/posts",
  {
    title: "Minimalist living room",
    attachments: [{ url: upload.publicUrl, type: upload.type }],
    description: "AI-generated interior concept",
  },
  `post-${TODAY}`,
);
console.log(post.data.url);

Common errors

StatusCauseWhat to do
400The body is invalid: a missing title, a media URL that is not https, or a type other than IMAGE or VIDEO. The message names the field.Fix the field named in the error message. Nothing was written and no quota was spent, so do not retry the same body.
401The API key is missing, malformed, revoked or sent in the wrong header.fetch does not add the header for you: set it in the headers object of every call. The header must read Authorization: Bearer followed by your ppk_live_ key. Check the key in Settings → Developer.
403The key lacks a scope the call needs: the answer is 403 with error.code insufficient_scope and the missing scope in error.required.Create a new key in Settings → Developer with the `publish` scope ticked and use it instead; repeating the call with the same key fails the same way.
429A per-minute or per-day limit was reached. The Retry-After header and the retryAfter field of the body say how many seconds to wait.Wait that long and repeat with the SAME Idempotency-Key. The helper above waits for a short Retry-After and throws on a long one, which is a daily limit.
502The server could not download the source URL: it answered 4xx or 5xx, the host does not resolve or the response was compressed.Open the source URL in a private browser window. It must be public and answer 200 without a login.

The full list of statuses, the retry rules and the limits are in Errors and retries.

Other tools