Skip to content
WebrenewDocs

Store your files

Manage your site's files with the Storage API

Upload, organize, and remove your site's photos and PDFs from your own scripts and tools. Use a personal API key.

Who this helps
Owners and developers who move many files, or who connect a photo workflow to their site.
Before starting
Have a site id, a terminal with curl, and permission to edit the site.
Done when
Your tool lists, uploads, changes, and deletes the site's files with its own API key.

01

Create an API key

A personal API key lets your own scripts and tools manage your site's files. You do not share your password.

A key acts as you. It works only on sites that you own or edit, and only while you still have that access. Each person can have 20 active keys.

  1. Open Settings in your dashboard, then the API keys tab.
  2. Select New key. Give the key a name that says where you use it, such as Gallery sync script.
  3. Choose the access. Read only lists files and reads usage (storage:read). Read and write also uploads, changes, and deletes files (storage:write).
  4. Choose the sites: All my sites, or Chosen sites (up to 50).
  5. Choose when the key expires: Never, 30 days, 90 days, or 1 year.
  6. Select Create key. Copy the key at once. Webrenew shows it only one time and keeps only a hash of it.
  7. To stop a key, select Revoke, then Revoke now. The key stops working at once.

Keep keys out of browser code and public repositories. Give each tool its own key with only the access that it needs. Then you can revoke one tool and keep the others. Keys cannot make or revoke keys: /api/builder/api-keys accepts only your dashboard session.

02

Send the key with each call

Every endpoint is under https://webrenew.com/api/builder/sites/{siteId}/storage. Replace {siteId} with the site's id. The id is the last part of the editor address, /dashboard/builder/{siteId}.

Send the key in the Authorization header: Authorization: Bearer wr_… A key is 46 characters: wr_ and then 43 letters, digits, dashes, and underscores. GET calls need storage:read. POST, PATCH, and DELETE calls need storage:write.

Every error is JSON with error, a message for people, and code, for programs. Some codes add fields, such as usedBytes and limitBytes. A problem on our side returns 500 with the code server_error. Try again later.

The dashboard uses the same API with your session. A write with a session must come from the dashboard. From anywhere else, use an API key.

Terminal
export WEBRENEW_API_KEY="wr_…"
export SITE_ID="your-site-id"

curl -H "Authorization: Bearer $WEBRENEW_API_KEY" \
  "https://webrenew.com/api/builder/sites/$SITE_ID/storage/usage"

03

Limits

Reads (GET), per API key or signed-in user: 1,200 requests an hour.

Writes (POST, PATCH, DELETE), per API key or signed-in user: 600 requests an hour.

Over a rate limit, the API returns 429 rate_limited with retryAfterSeconds and a Retry-After header.

Photos: JPG, PNG, or WebP. Webrenew stores each photo as WebP, at most 2400 pixels wide or high and 2 MB or less. PDFs: up to 25 MB. You cannot upload video.

Upload a file takes files up to 4 MB. A large upload takes files up to 25 MB. A site can have 20 large uploads that are not complete, and the owner's storage left must hold a 25 MB file for each one. Past either limit, starting one returns 429 too_many_uploads with a Retry-After header.

Keys: up to 512 characters. Use letters, digits, dots, dashes, and underscores, with / between folders. No empty, . or .. part. Folders are key prefixes, such as photos/.

Alt text: up to 300 characters. Metadata: a JSON object up to 4 KB.

Uploads count toward the site owner's storage, also when an editor or an editor's key uploads. At the limit, uploads stop with 402 over_limit. Webrenew deletes nothing.

Error 402
{
  "error": "Not enough storage: 1 GB of 1 GB used, and this file needs 1.8 MB. Add storage on the plan page (/dashboard/settings/usage) or delete files you no longer need.",
  "code": "over_limit",
  "usedBytes": 999600000,
  "limitBytes": 1000000000
}

04

The file object

Each endpoint that returns a file returns this object. The url is the file's public address. Use it on your pages or in other tools.

A photo also has variants: smaller copies, smallest first. Use them in a srcset, so each screen loads the size that it needs. The copies count toward storage, so usage grows by more than the photo's bytes.

Fields
NameTypeDescription
iduuidThe file's id. Use it in the object paths.
keystringThe name and folder you see, such as photos/beach.webp. Unique on the site.
namestringThe original file name.
contentType"image/webp" | "application/pdf"What Webrenew stores. Every photo is WebP.
bytesintegerThe stored size. It counts toward storage. The resized copies in variants count too.
width, heightinteger | nullThe photo's size in pixels. Null for a PDF.
altstring | nullWhat the photo shows.
metadataobjectYour own JSON. Webrenew does not read it.
sha256string | nullA hash of the stored bytes.
urlstringThe file's public address. A new key does not change it. Use the value that the API returns; the host can change.
variants{ width: integer, bytes: integer, url: string }[]Smaller copies of a photo, for phones and tablets, smallest first. Each has its width in pixels, its size, and its public address. Their bytes count toward storage. Empty for a PDF, and for a photo stored before Webrenew made copies.
createdAt, updatedAtISO 8601 stringWhen the file was added and last changed.
File object
{
  "id": "6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11",
  "key": "photos/beach-6f1c2a.webp",
  "name": "beach.jpg",
  "contentType": "image/webp",
  "bytes": 412806,
  "width": 2400,
  "height": 1600,
  "alt": "Sunrise over the beach at Port Aransas",
  "metadata": {
    "shoot": "2026-10"
  },
  "sha256": "9b74c9897bac770ffc029102a200c5de0f5a4b3ac0d6e1d0f8b9d6d27d0c3c7e",
  "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11.webp",
  "variants": [
    {
      "width": 480,
      "bytes": 28412,
      "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@480w.webp"
    },
    {
      "width": 960,
      "bytes": 96530,
      "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@960w.webp"
    },
    {
      "width": 1600,
      "bytes": 231904,
      "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@1600w.webp"
    }
  ],
  "createdAt": "2026-10-27T15:04:05.000Z",
  "updatedAt": "2026-10-27T15:04:05.000Z"
}

05

List files

GET /objects. Scope: storage:read.

Lists the site's files in key order. If nextCursor is not null, send it as cursor to get the next page.

curl
curl -H "Authorization: Bearer $WEBRENEW_API_KEY" \
  "https://webrenew.com/api/builder/sites/$SITE_ID/storage/objects?prefix=photos/&limit=100"

GET/api/builder/sites/{siteId}/storage/objects

Query parameters
NameTypeDescription
prefixstringOnly keys that start with this text, such as photos/. Folders are key prefixes. Letters, digits, and _ . / - only.
cursorstringThe nextCursor value from the previous page. Do not make your own.
limitintegerFiles per page, 1 to 100. The default is 50.
Response 200
{
  "objects": [
    {
      "id": "6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11",
      "key": "photos/beach-6f1c2a.webp",
      "name": "beach.jpg",
      "contentType": "image/webp",
      "bytes": 412806,
      "width": 2400,
      "height": 1600,
      "alt": "Sunrise over the beach at Port Aransas",
      "metadata": {
        "shoot": "2026-10"
      },
      "sha256": "9b74c9897bac770ffc029102a200c5de0f5a4b3ac0d6e1d0f8b9d6d27d0c3c7e",
      "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11.webp",
      "variants": [
        {
          "width": 480,
          "bytes": 28412,
          "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@480w.webp"
        },
        {
          "width": 960,
          "bytes": 96530,
          "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@960w.webp"
        },
        {
          "width": 1600,
          "bytes": 231904,
          "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@1600w.webp"
        }
      ],
      "createdAt": "2026-10-27T15:04:05.000Z",
      "updatedAt": "2026-10-27T15:04:05.000Z"
    }
  ],
  "nextCursor": "cGhvdG9zL2JlYWNoLTZmMWMyYS53ZWJw"
}
Errors
StatusCodeWhen
400invalid_requestA field or parameter is not valid. The message names it.
401unauthorizedThere is no key, the key has the wrong form, or the key is not valid, revoked, or expired. The message says which.
403forbiddenThe key does not have the scope, or the key does not allow this site.
404not_foundThe site does not exist, or you cannot edit it. Also a file id that is not on this site.
429rate_limitedToo many requests this hour. Wait for retryAfterSeconds, also sent as Retry-After.

06

Upload a file

POST /objects. Scope: storage:write.

Uploads one photo or PDF of up to 4 MB. Webrenew reads the type from the bytes. Photos become WebP of 2 MB or less. Use a large upload for bigger files.

curl
curl -H "Authorization: Bearer $WEBRENEW_API_KEY" \
  -F "file=@beach.jpg" -F "key=photos/beach.webp" -F "alt=Sunrise over the beach" \
  "https://webrenew.com/api/builder/sites/$SITE_ID/storage/objects"

POST/api/builder/sites/{siteId}/storage/objects

Request body (multipart/form-data)
NameTypeDescription
file(required)fileA JPG, PNG, WebP, or PDF, up to 4 MB.
keystringWhere to store it, such as photos/beach.webp. Without a key, a photo gets photos/<name>-<6 characters>.webp and a PDF gets files/<name>-<6 characters>.pdf.
altstringWhat the photo shows, up to 300 characters.
metadatastringA JSON object as text, up to 4 KB.
Response 201
{
  "object": {
    "id": "6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11",
    "key": "photos/beach-6f1c2a.webp",
    "name": "beach.jpg",
    "contentType": "image/webp",
    "bytes": 412806,
    "width": 2400,
    "height": 1600,
    "alt": "Sunrise over the beach at Port Aransas",
    "metadata": {
      "shoot": "2026-10"
    },
    "sha256": "9b74c9897bac770ffc029102a200c5de0f5a4b3ac0d6e1d0f8b9d6d27d0c3c7e",
    "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11.webp",
    "variants": [
      {
        "width": 480,
        "bytes": 28412,
        "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@480w.webp"
      },
      {
        "width": 960,
        "bytes": 96530,
        "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@960w.webp"
      },
      {
        "width": 1600,
        "bytes": 231904,
        "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@1600w.webp"
      }
    ],
    "createdAt": "2026-10-27T15:04:05.000Z",
    "updatedAt": "2026-10-27T15:04:05.000Z"
  }
}
Errors
StatusCodeWhen
400invalid_requestA field or parameter is not valid. The message names it.
400invalid_keyThe key breaks the key rule.
402over_limitThe file does not fit in the owner's storage. The body adds usedBytes and limitBytes. Webrenew deletes nothing.
409key_existsAnother file on the site has this key.
413too_largeThe file is over its size limit, also after Webrenew resizes it.
415unsupported_typeThe bytes are not a JPG, PNG, WebP, or PDF, or the photo cannot be read.
401unauthorizedThere is no key, the key has the wrong form, or the key is not valid, revoked, or expired. The message says which.
403forbiddenThe key does not have the scope, or the key does not allow this site.
404not_foundThe site does not exist, or you cannot edit it. Also a file id that is not on this site.
429rate_limitedToo many requests this hour. Wait for retryAfterSeconds, also sent as Retry-After.

07

Start a large upload

POST /uploads. Scope: storage:write.

For files over 4 MB, up to 25 MB. Send no body. Then PUT the raw bytes to uploadUrl with the file's Content-Type, within 2 hours. Then complete the upload. The file does not count toward storage until you complete it.

curl
curl -H "Authorization: Bearer $WEBRENEW_API_KEY" -X POST \
  "https://webrenew.com/api/builder/sites/$SITE_ID/storage/uploads"

# Send the bytes to the uploadUrl from the response
curl -X PUT -H "Content-Type: application/pdf" --data-binary @menu.pdf "$UPLOAD_URL"

POST/api/builder/sites/{siteId}/storage/uploads

Response 201
{
  "uploadId": "1d3e7c52-9a0b-4f61-8c2d-5e7f9a1b3c4d",
  "uploadUrl": "https://…/object/upload/sign/builder-uploads/builder/0b7d…/1d3e7c52-…?token=…",
  "token": "eyJhbGciOi…",
  "expiresAt": "2026-10-27T17:04:05.000Z"
}
Errors
StatusCodeWhen
402over_limitThe owner's storage is already full. The body adds usedBytes and limitBytes.
429too_many_uploadsToo many large uploads wait to be completed, for one of two reasons: the site has 20 uploads that are not complete, or the owner's storage left cannot hold a 25 MB file for each upload that waits. Complete them, or wait until one expires. retryAfterSeconds, also sent as Retry-After, is the time until the oldest one expires (at most 2 hours).
401unauthorizedThere is no key, the key has the wrong form, or the key is not valid, revoked, or expired. The message says which.
403forbiddenThe key does not have the scope, or the key does not allow this site.
404not_foundThe site does not exist, or you cannot edit it. Also a file id that is not on this site.
429rate_limitedToo many requests this hour. Wait for retryAfterSeconds, also sent as Retry-After.
503unavailableStorage did not answer. Nothing was started: start the upload again after retryAfterSeconds, also sent as Retry-After.

08

Complete a large upload

POST /uploads/{uploadId}/complete. Scope: storage:write.

Checks the size and reads the type from the bytes. A photo becomes WebP. A PDF must start with %PDF-. Then Webrenew stores the file. The body can be empty. Webrenew deletes the staged copy, unless storage did not answer (503): then you can complete the upload again.

curl
curl -H "Authorization: Bearer $WEBRENEW_API_KEY" -H "Content-Type: application/json" \
  -d '{"key":"files/menu.pdf","name":"menu.pdf"}' \
  "https://webrenew.com/api/builder/sites/$SITE_ID/storage/uploads/$UPLOAD_ID/complete"

POST/api/builder/sites/{siteId}/storage/uploads/{uploadId}/complete

Path parameters
NameTypeDescription
uploadId(required)uuidThe uploadId from Start a large upload.
Request body (application/json)
NameTypeDescription
keystringWhere to store the file. The default follows the same rule as Upload a file.
namestringThe original file name, 1 to 200 characters. The default key uses it.
altstringWhat the photo shows, up to 300 characters.
metadataobjectYour own JSON, up to 4 KB.
Response 201
{
  "object": {
    "id": "6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11",
    "key": "files/menu.pdf",
    "name": "menu.pdf",
    "contentType": "application/pdf",
    "bytes": 9800000,
    "width": null,
    "height": null,
    "alt": null,
    "metadata": {
      "shoot": "2026-10"
    },
    "sha256": "9b74c9897bac770ffc029102a200c5de0f5a4b3ac0d6e1d0f8b9d6d27d0c3c7e",
    "url": "https://media.webrenew.com/builder/0b7d…/9a8b7c6d-….pdf",
    "variants": [],
    "createdAt": "2026-10-27T15:04:05.000Z",
    "updatedAt": "2026-10-27T15:04:05.000Z"
  }
}
Errors
StatusCodeWhen
404upload_not_foundNo file arrived at uploadUrl for this uploadId, or the upload is already complete.
400invalid_requestA field or parameter is not valid. The message names it.
400invalid_keyThe key breaks the key rule.
402over_limitThe file does not fit in the owner's storage. The body adds usedBytes and limitBytes. Webrenew deletes nothing.
409key_existsAnother file on the site has this key.
413too_largeThe file is over its size limit, also after Webrenew resizes it.
415unsupported_typeThe bytes are not a JPG, PNG, WebP, or PDF, or the photo cannot be read.
401unauthorizedThere is no key, the key has the wrong form, or the key is not valid, revoked, or expired. The message says which.
403forbiddenThe key does not have the scope, or the key does not allow this site.
404not_foundThe site does not exist, or you cannot edit it. Also a file id that is not on this site.
429rate_limitedToo many requests this hour. Wait for retryAfterSeconds, also sent as Retry-After.
503unavailableStorage did not answer. Webrenew keeps the staged file: complete the same uploadId again after retryAfterSeconds, also sent as Retry-After.

09

Get a file

GET /objects/{objectId}. Scope: storage:read.

Returns one file's details and its public address.

curl
curl -H "Authorization: Bearer $WEBRENEW_API_KEY" \
  "https://webrenew.com/api/builder/sites/$SITE_ID/storage/objects/$FILE_ID"

GET/api/builder/sites/{siteId}/storage/objects/{objectId}

Path parameters
NameTypeDescription
objectId(required)uuidThe file's id.
Response 200
{
  "object": {
    "id": "6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11",
    "key": "photos/beach-6f1c2a.webp",
    "name": "beach.jpg",
    "contentType": "image/webp",
    "bytes": 412806,
    "width": 2400,
    "height": 1600,
    "alt": "Sunrise over the beach at Port Aransas",
    "metadata": {
      "shoot": "2026-10"
    },
    "sha256": "9b74c9897bac770ffc029102a200c5de0f5a4b3ac0d6e1d0f8b9d6d27d0c3c7e",
    "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11.webp",
    "variants": [
      {
        "width": 480,
        "bytes": 28412,
        "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@480w.webp"
      },
      {
        "width": 960,
        "bytes": 96530,
        "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@960w.webp"
      },
      {
        "width": 1600,
        "bytes": 231904,
        "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@1600w.webp"
      }
    ],
    "createdAt": "2026-10-27T15:04:05.000Z",
    "updatedAt": "2026-10-27T15:04:05.000Z"
  }
}
Errors
StatusCodeWhen
401unauthorizedThere is no key, the key has the wrong form, or the key is not valid, revoked, or expired. The message says which.
403forbiddenThe key does not have the scope, or the key does not allow this site.
404not_foundThe site does not exist, or you cannot edit it. Also a file id that is not on this site.
429rate_limitedToo many requests this hour. Wait for retryAfterSeconds, also sent as Retry-After.

10

Update a file

PATCH /objects/{objectId}. Scope: storage:write.

Renames or moves a file (key), or changes its alt text or metadata. Send at least one of these fields, and no other fields. The file's url does not change.

curl
curl -H "Authorization: Bearer $WEBRENEW_API_KEY" -X PATCH -H "Content-Type: application/json" \
  -d '{"key":"gallery/beach.webp","alt":"Sunrise over the beach"}' \
  "https://webrenew.com/api/builder/sites/$SITE_ID/storage/objects/$FILE_ID"

PATCH/api/builder/sites/{siteId}/storage/objects/{objectId}

Path parameters
NameTypeDescription
objectId(required)uuidThe file's id.
Request body (application/json)
NameTypeDescription
keystringThe new key.
altstring | nullNew alt text. Null clears it.
metadataobjectYour own JSON, up to 4 KB. It replaces all of the old metadata.
Response 200
{
  "object": {
    "id": "6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11",
    "key": "gallery/beach.webp",
    "name": "beach.jpg",
    "contentType": "image/webp",
    "bytes": 412806,
    "width": 2400,
    "height": 1600,
    "alt": "Sunrise over the beach at Port Aransas",
    "metadata": {
      "shoot": "2026-10"
    },
    "sha256": "9b74c9897bac770ffc029102a200c5de0f5a4b3ac0d6e1d0f8b9d6d27d0c3c7e",
    "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11.webp",
    "variants": [
      {
        "width": 480,
        "bytes": 28412,
        "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@480w.webp"
      },
      {
        "width": 960,
        "bytes": 96530,
        "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@960w.webp"
      },
      {
        "width": 1600,
        "bytes": 231904,
        "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@1600w.webp"
      }
    ],
    "createdAt": "2026-10-27T15:04:05.000Z",
    "updatedAt": "2026-10-27T16:10:00.000Z"
  }
}
Errors
StatusCodeWhen
400invalid_requestA field or parameter is not valid. The message names it.
400invalid_keyThe key breaks the key rule.
409key_existsAnother file on the site has this key.
401unauthorizedThere is no key, the key has the wrong form, or the key is not valid, revoked, or expired. The message says which.
403forbiddenThe key does not have the scope, or the key does not allow this site.
404not_foundThe site does not exist, or you cannot edit it. Also a file id that is not on this site.
429rate_limitedToo many requests this hour. Wait for retryAfterSeconds, also sent as Retry-After.

11

Delete a file

DELETE /objects/{objectId}. Scope: storage:write.

Deletes a file and returns it. Webrenew keeps a file that the site still uses: in its latest files, in the live site, or in a post's blocks or cover. Add force=true to delete it anyway.

curl
curl -H "Authorization: Bearer $WEBRENEW_API_KEY" -X DELETE \
  "https://webrenew.com/api/builder/sites/$SITE_ID/storage/objects/$FILE_ID"

# Delete it even when a page uses it
curl -H "Authorization: Bearer $WEBRENEW_API_KEY" -X DELETE \
  "https://webrenew.com/api/builder/sites/$SITE_ID/storage/objects/$FILE_ID?force=true"

DELETE/api/builder/sites/{siteId}/storage/objects/{objectId}

Path parameters
NameTypeDescription
objectId(required)uuidThe file's id.
Query parameters
NameTypeDescription
forcebooleantrue deletes the file even when the site uses it. Those pages then show no image.
Response 200
{
  "object": {
    "id": "6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11",
    "key": "photos/beach-6f1c2a.webp",
    "name": "beach.jpg",
    "contentType": "image/webp",
    "bytes": 412806,
    "width": 2400,
    "height": 1600,
    "alt": "Sunrise over the beach at Port Aransas",
    "metadata": {
      "shoot": "2026-10"
    },
    "sha256": "9b74c9897bac770ffc029102a200c5de0f5a4b3ac0d6e1d0f8b9d6d27d0c3c7e",
    "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11.webp",
    "variants": [
      {
        "width": 480,
        "bytes": 28412,
        "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@480w.webp"
      },
      {
        "width": 960,
        "bytes": 96530,
        "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@960w.webp"
      },
      {
        "width": 1600,
        "bytes": 231904,
        "url": "https://media.webrenew.com/builder/0b7d…/6f1c2a9e-3b0d-4c55-9a51-0f4f7a1d2c11@1600w.webp"
      }
    ],
    "createdAt": "2026-10-27T15:04:05.000Z",
    "updatedAt": "2026-10-27T15:04:05.000Z"
  }
}
Errors
StatusCodeWhen
409in_useThe site uses the file. The body adds usedBy: {"files":["app/page.tsx"],"posts":["hello-world"]}.
401unauthorizedThere is no key, the key has the wrong form, or the key is not valid, revoked, or expired. The message says which.
403forbiddenThe key does not have the scope, or the key does not allow this site.
404not_foundThe site does not exist, or you cannot edit it. Also a file id that is not on this site.
429rate_limitedToo many requests this hour. Wait for retryAfterSeconds, also sent as Retry-After.

12

Get storage usage

GET /usage. Scope: storage:read.

Returns the storage that the site's owner uses on all their sites, and their limit. limitBytes is null when there is no limit. addonBytes is the part of the limit that add-ons give. The route returns exactly these three fields.

curl
curl -H "Authorization: Bearer $WEBRENEW_API_KEY" \
  "https://webrenew.com/api/builder/sites/$SITE_ID/storage/usage"

GET/api/builder/sites/{siteId}/storage/usage

Response 200
{
  "usage": {
    "usedBytes": 3412806112,
    "limitBytes": 150000000000,
    "addonBytes": 100000000000
  }
}
Errors
StatusCodeWhen
401unauthorizedThere is no key, the key has the wrong form, or the key is not valid, revoked, or expired. The message says which.
403forbiddenThe key does not have the scope, or the key does not allow this site.
404not_foundThe site does not exist, or you cannot edit it. Also a file id that is not on this site.
429rate_limitedToo many requests this hour. Wait for retryAfterSeconds, also sent as Retry-After.

13

Example: sync a folder

This script uploads each photo and PDF in a local folder that the site does not have yet. Run it again after each shoot. It skips the files that are already there.

It lists the site's files one time, then uploads only the new ones. Files over 4 MB go through a large upload. An error stops the script and shows the API's code and message.

sync.ts
// sync.ts: upload the new photos and PDFs in a folder.
// Run with Node.js 22 or newer: npx tsx sync.ts
import { readdir, readFile } from "node:fs/promises"
import { extname, join } from "node:path"

const BASE = `https://webrenew.com/api/builder/sites/${process.env.SITE_ID}/storage`
const auth = { Authorization: `Bearer ${process.env.WEBRENEW_API_KEY}` }
const TYPES: Record<string, string> = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp", ".pdf": "application/pdf" }
const DIRECT_LIMIT = 4194304

async function call<T>(path: string, init: RequestInit = {}): Promise<T> {
  const response = await fetch(`${BASE}${path}`, { ...init, headers: { ...auth, ...init.headers } })
  const body = await response.json()
  if (!response.ok) throw new Error(`${path}: ${body.code}: ${body.error}`)
  return body as T
}

/** Photos are stored as WebP, so beach.jpg becomes photos/beach.webp. */
function keyFor(folder: string, name: string): string {
  const ext = extname(name).toLowerCase()
  const base = name.slice(0, -ext.length).replace(/[^A-Za-z0-9_.-]+/g, "-")
  return `${folder}/${base}${ext === ".pdf" ? ".pdf" : ".webp"}`
}

async function storedKeys(prefix: string): Promise<Set<string>> {
  const keys = new Set<string>()
  let cursor: string | null = null
  do {
    const query = new URLSearchParams({ prefix, limit: "100", ...(cursor ? { cursor } : {}) })
    const page = await call<{ objects: { key: string }[]; nextCursor: string | null }>(`/objects?${query}`)
    for (const object of page.objects) keys.add(object.key)
    cursor = page.nextCursor
  } while (cursor)
  return keys
}

async function upload(bytes: Buffer, name: string, key: string, type: string): Promise<void> {
  if (bytes.length <= DIRECT_LIMIT) {
    const form = new FormData()
    form.set("file", new Blob([bytes], { type }), name)
    form.set("key", key)
    await call("/objects", { method: "POST", body: form })
    return
  }
  // Larger files: start, PUT the bytes to the signed URL, then complete.
  const started = await call<{ uploadId: string; uploadUrl: string }>("/uploads", { method: "POST" })
  const put = await fetch(started.uploadUrl, { method: "PUT", headers: { "Content-Type": type }, body: bytes })
  if (!put.ok) throw new Error(`${name}: upload failed with ${put.status}`)
  await call(`/uploads/${started.uploadId}/complete`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, name }) })
}

const folder = "photos"
const stored = await storedKeys(`${folder}/`)
for (const name of await readdir(folder)) {
  const type = TYPES[extname(name).toLowerCase()]
  if (!type) continue
  const key = keyFor(folder, name)
  if (stored.has(key)) continue
  await upload(await readFile(join(folder, name)), name, key, type)
  console.log(`Uploaded ${key}`)
}