API architecture guide

How to implement an asynchronous image-editing API

Submit an image once, keep the returned job ID, wait for a terminal state by polling or signed webhook, then retrieve the result. Use the same idempotency key when retrying a submission so a transport failure does not create a second charged job. See the live OpenAPI 3.1 contract and the shipped integration skill.

Published and last source-verified August 11, 2026 · Peelaway Engineering

The four-step contract

  1. 1. Submit

    POST /api/process with a base64 JPEG, PNG, or WebP and a prompt. The response returns a job_id immediately.

  2. 2. Persist

    Store the job ID with your own work item. For submission retries, reuse the original Idempotency-Key.

  3. 3. Wait

    Poll /api/process/status every 2–5 seconds, or provide a public HTTPS webhook URL. The terminal states are done and error.

  4. 4. Retrieve

    After done, GET /api/process/result. Its output URL is signed and remains valid for one hour, so copy the bytes into storage you control.

Sources: request and response schemas and polling and result guidance.

Minimal curl workflow

Authentication uses a bearer API key. The submit endpoint accepts an optional client-supplied idempotency value; retain it until the submission outcome is known.

curl
# Generate once, persist it, and reuse it for any submit retry.
IDEMPOTENCY_KEY="$(uuidgen)"

# Submit one image.
curl -sS -X POST https://api.peelaway.io/api/process \
  -H "Authorization: Bearer $PEELAWAY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
    "image": "<base64-encoded JPEG, PNG, or WebP>",
    "prompt": "remove the temporary sign",
    "format": "jpeg"
  }'
# -> {"job_id":"...","credits_remaining":24}

# Poll until status is done or error.
curl -sS \
  -H "Authorization: Bearer $PEELAWAY_API_KEY" \
  "https://api.peelaway.io/api/process/status?job_id=<job_id>"
# -> {"status":"pending"}

# After done, fetch the signed result URL.
curl -sS \
  -H "Authorization: Bearer $PEELAWAY_API_KEY" \
  "https://api.peelaway.io/api/process/result?job_id=<job_id>"

Source: Peelaway OpenAPI.

TypeScript polling loop

This Node example polls every three seconds, which stays within the documented 2–5 second interval. Production code should also persist the job ID before entering the loop and apply its own overall timeout.

TypeScript
import { readFile } from "node:fs/promises";

const apiKey = process.env.PEELAWAY_API_KEY;
if (!apiKey) throw new Error("PEELAWAY_API_KEY is required");

const headers = {
  Authorization: "Bearer " + apiKey,
  "Content-Type": "application/json",
};
const image = (await readFile("input.jpg")).toString("base64");
const submission = await fetch("https://api.peelaway.io/api/process", {
  method: "POST",
  headers: { ...headers, "Idempotency-Key": crypto.randomUUID() },
  body: JSON.stringify({ image, prompt: "remove the temporary sign" }),
});
if (!submission.ok) throw new Error("submit failed: " + submission.status);
const { job_id: jobId } = await submission.json() as { job_id: string };

for (;;) {
  await new Promise((resolve) => setTimeout(resolve, 3_000));
  const statusResponse = await fetch(
    "https://api.peelaway.io/api/process/status?job_id=" + jobId,
    { headers },
  );
  if (!statusResponse.ok) throw new Error("status failed: " + statusResponse.status);
  const { status } = await statusResponse.json() as {
    status: "pending" | "done" | "error";
  };
  if (status === "pending") continue;
  if (status === "error") throw new Error("image edit failed");
  break;
}

const resultResponse = await fetch(
  "https://api.peelaway.io/api/process/result?job_id=" + jobId,
  { headers },
);
if (!resultResponse.ok) throw new Error("result failed: " + resultResponse.status);
const result = await resultResponse.json() as { url: string };
console.log(result.url);

Source: Peelaway integration skill.

Webhook verification and delivery

A webhook URL must be public HTTPS. Settlement callbacks carrydoneorerror, plus X-Peelaway-Timestamp and X-Peelaway-Signature headers. Fetch the per-user secret from GET /api/webhooks/secret, compute HMAC-SHA256 over the exact string ${timestamp}.${rawBody}, compare signatures, reject stale timestamps, and deduplicate on job ID.

Non-2xx and 3xx delivery responses are retried after 30 seconds, 2 minutes, 10 minutes, and 1 hour. A successful callback includes the signed result URL directly; an error callback contains a generic failure message instead.

Sources: callback schema and signature contract and delivery retry schedule.

Retry boundaries

ResponseMeaningClient action
400Invalid request or batch above the plan limitCorrect the input or reduce the batch; do not retry unchanged.
402Insufficient creditsAdd credits before retrying.
403Email is not verifiedComplete verification before submitting.
429Rate-limited or too many jobs in flightHonor Retry-After or wait for in-flight jobs to settle.
502Upstream provider failureRetry with the same Idempotency-Key.

Source: documented error handling. Status lookup can also return 400 or 404; result lookup returns 202 while processing, 404 for an unknown job, and 500 for a failed job, as listed in the OpenAPI responses.

Limits to design around

  • A request may contain up to 50 images, but the account plan can impose a lower concurrent-job and batch limit.
  • Peelaway edits an input image; it does not generate an image from scratch. Use images the caller owns or is authorized to edit.
  • Dimension behavior depends on the selected workflow. Validate representative outputs before removing human review.
  • Signed result links expire after one hour. Download or copy the finished bytes rather than treating the URL as permanent storage.

Sources: batch and result schemas, usage boundaries, and workflow limitations.

Choose the next integration step

Create a verified account and key for REST, review the developer workflow, or use the connector guide for an interactive MCP client.

Primary sources