Logotipo de Foto a URL

API

Upload a file from your own code and get a URL back. One key, three calls, no SDK to install.

Authentication

Every request carries your key as a bearer token. Create one on your dashboard.

Authorization: Bearer ptu_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Keys are shown once, when you create them. Send them in the header and never in a query string — a key in a URL ends up in access logs, browser history and referrer headers.

Uploading a file

Three steps: ask for a URL, send the bytes straight to storage, then publish. The bytes never pass through our API, which is why file size is limited by your plan rather than by a request body limit.

curl
# 1. Ask for a URL to upload to.
curl -X POST https://phototourl.org/api/v1/uploads \
  -H "Authorization: Bearer $PHOTOTOURL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contentType":"image/png","size":48120,"fileName":"chart.png"}'

# -> {"id":"7f3c…","key":"pending/2026-09-17-7f3c….png",
#     "uploadUrl":"https://…","headers":{…},"completeUrl":"/api/v1/uploads/7f3c…/complete"}

# 2. PUT the bytes straight to storage, with the headers you were given.
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: image/png" \
  -H "Cache-Control: $CACHE_CONTROL" \
  --data-binary @chart.png

# 3. Publish it. This is where the file is checked and the link is minted.
curl -X POST https://phototourl.org/api/v1/uploads/7f3c…/complete \
  -H "Authorization: Bearer $PHOTOTOURL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"pending/2026-09-17-7f3c….png","contentType":"image/png","fileName":"chart.png"}'

# -> {"upload":{"id":"7f3c…","url":"https://…","sizeBytes":48120, …}}
JavaScript
async function upload(file, key) {
  const headers = { Authorization: `Bearer ${key}`, "Content-Type": "application/json" };

  // 1. Authorize.
  const start = await fetch("https://phototourl.org/api/v1/uploads", {
    method: "POST",
    headers,
    body: JSON.stringify({ contentType: file.type, size: file.size, fileName: file.name }),
  });
  if (!start.ok) throw new Error((await start.json()).error);
  const { id, key: objectKey, uploadUrl, headers: putHeaders } = await start.json();

  // 2. Send the bytes to storage. They never pass through the API, which is
  //    what lets this handle files far larger than a normal request body.
  const put = await fetch(uploadUrl, { method: "PUT", headers: putHeaders, body: file });
  if (!put.ok) throw new Error("Upload failed");

  // 3. Publish.
  const done = await fetch(`https://phototourl.org/api/v1/uploads/${id}/complete`, {
    method: "POST",
    headers,
    body: JSON.stringify({ key: objectKey, contentType: file.type, fileName: file.name }),
  });
  if (!done.ok) throw new Error((await done.json()).error);
  return (await done.json()).upload.url;
}

Naming, expiring and locking a link

The same controls the dashboard offers. Fields you leave out are untouched; null clears one. Your plan decides which are available.

curl
curl -X PATCH https://phototourl.org/api/v1/uploads/7f3c… \
  -H "Authorization: Bearer $PHOTOTOURL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"slug":"q3-revenue","password":"hunter2","expiresAt":"2027-01-01T00:00:00Z"}'

Endpoints

GET/api/v1/meYour plan, limits and usage.
GET/api/v1/uploadsYour uploads, newest first.
POST/api/v1/uploadsAuthorize an upload and get a URL to PUT to.
POST/api/v1/uploads/{id}/completeValidate what landed and publish it.
GET/api/v1/uploads/{id}One upload.
PATCH/api/v1/uploads/{id}Set a slug, expiry, password or protection.
DELETE/api/v1/uploads/{id}Delete the file and its link.
POST/api/v1/uploads/{id}/fileAuthorize a replacement for an existing link.
PUT/api/v1/uploads/{id}/fileSwap the bytes, keeping the URL.

Limits

Uploads are counted against your account, not against each key — issuing more keys does not raise the allowance. Replacing a file costs nothing.

PlanUploadsMax sizeAPI
plus · £4500 / month50MBNot included
pro · £122,000 / month100MBNot included
business · £2510,000 / month250MBIncluded

Errors

Every failure is JSON with an `error` field, plus whatever context is useful.

{ "error": "Monthly upload limit reached (2000 per month).", "limit": 2000, "window": "month" }
  • 401The key is missing, wrong or revoked.
  • 403Your plan does not include this.
  • 413The file is larger than your plan allows.
  • 429You have used this period's uploads.