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
- 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.
- Write one helper that adds the Authorization header and the Idempotency-Key, and retries on 429 and 503 after Retry-After.
- Upload with POST /api/v1/uploads/from-url, then create the post with POST /api/v1/posts using data.publicUrl.
- Call the API from a server only. A key in browser code is visible to every visitor.
- 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
| Status | Cause | What to do |
|---|---|---|
| 400 | The 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. |
| 401 | The 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. |
| 403 | The 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. |
| 429 | A 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. |
| 502 | The 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.