# Process images with the SocialCutter API from Node.js
> SocialCutter API client in Node 18+ with native fetch and FormData: health, platforms, processing by URL and file, a folder batch script and error handling.
- URL: https://socialcutter.theboomer.dev/en/guides/node/
- Idioma: en
- Familia: api
- Actualizado: 2026-09-24
- Palabras clave: Node.js, fetch, FormData, API, SocialCutter, process images, API key
## Requirements

Node **18 or higher**. In that version `fetch`, `FormData`, `Blob` and `Headers` are globals, so no external package is needed. Keep the credentials in environment variables:

```bash
export SOCIALCUTTER_API_URL="https://api.socialcutter.theboomer.dev"
export SOCIALCUTTER_API_KEY="sc_your_key"
```

Create the key at https://dash.socialcutter.theboomer.dev under **Profile → API keys**. It starts with `sc_` and is shown only once. The full reference is at https://docs.socialcutter.theboomer.dev.

## Minimal client

```javascript
const API_URL = process.env.SOCIALCUTTER_API_URL ?? 'https://api.socialcutter.theboomer.dev'
const API_KEY = process.env.SOCIALCUTTER_API_KEY // never hardcode it

async function call(path, init = {}) {
  const res = await fetch(`${API_URL}${path}`, {
    ...init,
    headers: { 'X-API-Key': API_KEY, ...(init.headers ?? {}) }
  })
  if (!res.ok) {
    const body = await res.text()
    throw new Error(`HTTP ${res.status}: ${body}`)
  }
  return res.json()
}

const getJson = (path) => call(path)
const postJson = (path, payload) =>
  call(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) })
```

`fetch` does not throw on 4xx or 5xx responses: you must check `res.ok` explicitly, as above.

## Health and credentials

```javascript
// Public: no key required
console.log(await getJson('/api/v1/health'))   // status, version, uptime, database

// Identity of the authenticated account
console.log(await getJson('/api/v1/auth/me'))

// Available uses
console.log(await getJson('/api/v1/credits'))
```

If `/api/v1/auth/me` returns 401, the key is missing, malformed or revoked.

## List platforms and formats

```javascript
const { platforms } = await getJson('/api/v1/platforms')
for (const [name, formats] of Object.entries(platforms)) {
  for (const f of formats) {
    console.log(name, f.format, `${f.width}x${f.height}`, f.aspect_ratio)
  }
}
```

`/platforms`, `/formats` and `/fit-modes` are public. Use them to build destinations without hardcoding dimensions.

## Process an image by URL

```javascript
const resp = await postJson('/api/v1/images/process', {
  source: { type: 'url', value: 'https://example.com/photo.jpg' },
  destinations: [
    { platform: 'instagram', format: 'post' },
    { platform: 'tiktok', format: 'cover' }
  ]
})
console.log(resp.id, resp.status)
```

## Process a local file (multipart)

```javascript
import { readFile } from 'node:fs/promises'
import { basename } from 'node:path'

async function processFile(path) {
  const buffer = await readFile(path)
  const form = new FormData()
  form.append('file', new Blob([buffer], { type: 'image/jpeg' }), basename(path))
  form.append('destinations', JSON.stringify([{ platform: 'linkedin', format: 'post' }]))

  // Do not set Content-Type yourself: fetch writes the correct boundary
  return call('/api/v1/images/process/upload', { method: 'POST', body: form })
}
```

Let `fetch` compute the `Content-Type`: if you set it yourself, the multipart `boundary` is lost and the API cannot read the file. The upload limit is 5 MB; above that the API returns 413.

## Folder batch script

```javascript
import { readdir } from 'node:fs/promises'
import { join, extname } from 'node:path'

const EXT = new Set(['.jpg', '.jpeg', '.png', '.webp'])

async function batchFromFolder(folder, destinations) {
  const files = (await readdir(folder)).filter((f) => EXT.has(extname(f).toLowerCase()))
  const images = []
  for (const f of files) {
    const buffer = await readFile(join(folder, f))
    const b64 = buffer.toString('base64')
    images.push({
      source: { type: 'base64', value: b64 },
      destinations
    })
  }
  return postJson('/api/v1/images/batch', { images })
}

const out = await batchFromFolder('./photos', [{ platform: 'instagram', format: 'post' }])
console.log(`Batch with ${out.length ?? 'several'} results`)
```

Remember: 1 use per destination per image. A batch of 10 images with 2 destinations each is 20 uses.

## Read the response and download

The response carries the job id in `id`, a `status` and `outputs`, one entry per destination with `platform`, `format`, `url`, `width`, `height` and `size_bytes`.

```javascript
import { writeFile } from 'node:fs/promises'

for (const out of resp.outputs) {
  console.log(out.platform, out.format, `${out.width}x${out.height}`)
  const img = await fetch(out.url)
  const bytes = Buffer.from(await img.arrayBuffer())
  await writeFile(`${out.platform}-${out.format}.webp`, bytes)
}
```

## History and wallet

```javascript
// Last 10 images
console.log(await getJson('/api/v1/history?limit=10'))

// Only those created from the API
console.log(await getJson('/api/v1/history?origin=api&limit=10'))

// Daily quota, extra pool and purchased balance
console.log(await getJson('/api/v1/wallet'))
```

`limit` accepts 1 to 100 and `skip` pages through results. The `origin` filter separates `browser` (dashboard) from `api`.

## Errors and retries with backoff

```javascript
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))

async function withRetries(fn, attempts = 4, base = 500) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn()
    } catch (e) {
      const code = Number((e.message.match(/HTTP (\d+)/) ?? [])[1])
      if (code === 429 || code >= 500) {
        if (i === attempts - 1) throw e
        await sleep(base * 2 ** i)   // 500, 1000, 2000, 4000 ms
        continue
      }
      throw e
    }
  }
}
```

Do not retry 400, 401, 404 or 422: repeating the same request will not fix them. Only 429 (quota) and 5xx (temporary failure) deserve another attempt.

## Cost

- 1 use per destination (platform and format) per request.
- Repeated destinations in the same request are not charged twice.
- Failed processings are refunded.

## Typical errors

| Code | Meaning |
|---|---|
| 400 | Invalid payload: unknown platform, format or mode, malformed JSON, or a key already active |
| 401 | Missing, malformed, expired or revoked credentials |
| 404 | Resource not found (image or file id) |
| 413 | The file exceeds 5 MB |
| 422 | Request validation error |
| 429 | Wallet quota exhausted |
| 500 | Processing failure; uses from that request are refunded |

## Next steps

- Terminal guide: [Process images with the API from the terminal (curl)](/en/guides/curl/)
- Python guide: [Process images with the SocialCutter API from Python](/en/guides/python/)
- PHP guide: [Process images with the SocialCutter API from PHP](/en/guides/php/)
- API documentation: https://docs.socialcutter.theboomer.dev
- Dashboard: https://dash.socialcutter.theboomer.dev