<!DOCTYPE html>

[Link in Seconds](https://linkinseconds.com/)

[Pricing](https://linkinseconds.com/pricing)

[Log in](https://linkinseconds.com/login)[Sign up free](https://linkinseconds.com/signup)

[All articles](https://linkinseconds.com/blog)

# Upload a file by API and get a public link (curl, Node, Python)

[Sunny](https://linkinseconds.com/about#who-runs-this)·October 4, 2026· 7 min read

[#ai](https://linkinseconds.com/blog/topic/ai-agents-and-developers)#api#upload#file

Upload a file by API, get a link

report.pdf Live

lisview.com/report

To upload a file by API and get a public link, make three HTTP calls. First, `POST /api/v1/uploads` with the file's name, type and size to get a short-lived upload URL. Second, PUT the raw bytes to that URL. Third, `POST /api/v1/uploads/complete` and you get back a link like `https://lisview.com/report-x7` that opens in any browser, with no sign-in for the viewer. All you need is an API key from your dashboard.

Below are working examples in curl, Node and Python, followed by the parts that matter in real use: safe retries, view-only links, listing your links, errors and limits.

## Before you start: get an API key

Sign in, open [API keys](https://linkinseconds.com/dashboard/api-keys) and create one. It starts with `lis_` and is shown once, so copy it into an environment variable straight away:

Shell

```
export LIS_API_KEY=lis_YOUR_KEY
```

Send it on every call as `Authorization: Bearer lis_...`. The API only accepts this header, never your browser cookies, so a web page cannot quietly call it on your behalf. See [Authentication and API keys](https://linkinseconds.com/docs/api/authentication).

## Why three calls instead of one?

Try it while you are reading

Upload a file and get a clean, trackable link with a QR code. Free, and no account for whoever opens it.

[Create a link](https://linkinseconds.com/)

Because the file never passes through our API servers. Step 1 checks your key, plan and file details and hands back a signed URL. Step 2 sends the bytes directly to storage. Step 3 asks us to inspect what actually arrived and publish it. This is why the API can take files as large as your plan allows, rather than the few megabytes a single serverless request can carry.

- **POST /uploads** with `{ name, type, size }` returns `{ path, signedUrl }`.

- **PUT signedUrl** with the raw bytes and the same `Content-Type`. The URL is valid for 10 minutes and needs no API key.

- **POST /uploads/complete** with `{ path, name, type }` returns `{ slug, title, url }`.

## Upload with curl

This uses `jq` to read the JSON. Change `FILE` and `TYPE` for your file.

upload.sh

```
export LIS_API_KEY=lis_YOUR_KEY
FILE=report.pdf; TYPE=application/pdf
SIZE=$(wc -c < "$FILE" | tr -d ' ')

# 1. Ask for an upload URL
START=$(curl -s -X POST https://linkinseconds.com/api/v1/uploads \
  -H "Authorization: Bearer $LIS_API_KEY" -H "Content-Type: application/json" \
  -d "{\"name\":\"$FILE\",\"type\":\"$TYPE\",\"size\":$SIZE}")
URL=$(echo "$START" | jq -r .signedUrl)
UPLOAD_PATH=$(echo "$START" | jq -r .path)

# 2. PUT the bytes straight to storage
curl -s -X PUT -H "Content-Type: $TYPE" --data-binary @"$FILE" "$URL"

# 3. Complete the upload and get the link
curl -s -X POST https://linkinseconds.com/api/v1/uploads/complete \
  -H "Authorization: Bearer $LIS_API_KEY" -H "Content-Type: application/json" \
  -d "{\"path\":\"$UPLOAD_PATH\",\"name\":\"$FILE\",\"type\":\"$TYPE\"}"
```

The last call prints the link:

200 OK

```
{
  "slug": "report-x7",
  "title": "report",
  "url": "https://lisview.com/report-x7"
}
```

## Upload with Node.js

Node 18 or newer has `fetch` built in, so there is nothing to install.

publish.mjs

```
import { readFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";

const API = "https://linkinseconds.com/api/v1";
const headers = {
  Authorization: `Bearer ${process.env.LIS_API_KEY}`,
  "Content-Type": "application/json",
};

async function publish(file, type) {
  const bytes = await readFile(file);
  const idempotencyKey = randomUUID();

  const start = await fetch(`${API}/uploads`, {
    method: "POST",
    headers,
    body: JSON.stringify({ name: file, type, size: bytes.length, idempotencyKey }),
  }).then((r) => r.json());
  if (start.existing) return start.existing;
  if (!start.signedUrl) throw new Error(start.error);

  const put = await fetch(start.signedUrl, { method: "PUT", headers: { "Content-Type": type }, body: bytes });
  if (!put.ok) throw new Error("Storage upload failed");

  const done = await fetch(`${API}/uploads/complete`, {
    method: "POST",
    headers,
    body: JSON.stringify({ path: start.path, name: file, type, idempotencyKey }),
  }).then((r) => r.json());
  if (!done.url) throw new Error(done.error);
  return done;
}

console.log(await publish("report.pdf", "application/pdf"));
```

## Upload with Python

This uses the `requests` library (`pip install requests`).

publish.py

```
import os, uuid, requests

API = "https://linkinseconds.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['LIS_API_KEY']}"}

def publish(path, content_type):
    data = open(path, "rb").read()
    key = str(uuid.uuid4())

    start = requests.post(f"{API}/uploads", headers=HEADERS, json={
        "name": path, "type": content_type, "size": len(data), "idempotencyKey": key,
    }).json()
    if "existing" in start:
        return start["existing"]
    if "signedUrl" not in start:
        raise RuntimeError(start.get("error"))

    requests.put(start["signedUrl"], data=data,
                 headers={"Content-Type": content_type}).raise_for_status()

    done = requests.post(f"{API}/uploads/complete", headers=HEADERS, json={
        "path": start["path"], "name": path, "type": content_type, "idempotencyKey": key,
    }).json()
    if "url" not in done:
        raise RuntimeError(done.get("error"))
    return done

print(publish("report.pdf", "application/pdf"))
```

## Make retries safe with an idempotency key

Networks drop responses, especially on big files. If your script retries a step without care, you can end up with two links for the same file. The Node and Python examples avoid that by sending an `idempotencyKey` (any UUID) on step 1 and step 3. If that key already produced a link, step 1 answers with `{ "existing": { slug, title, url } }` and you can skip the rest. A retried step 3 returns the same link instead of a duplicate, and it never uses a second free slot.

## Useful options

- **linkName** on step 3 sets the title shown on the link page. It defaults to the file name.

- **allowDownload: false** on step 3 makes the link view-only, so the download button is hidden. Handy for a deck or a draft. Read more in [sharing a PDF people can view but not download](https://linkinseconds.com/blog/share-a-pdf-people-can-view-but-not-download).

- **slug** on both steps sets a custom link name, such as `lisview.com/q3-report`. It works on every plan.

Every field is listed in [Upload a file with the API](https://linkinseconds.com/docs/api/uploads), and the machine-readable version lives at [/openapi.json](https://linkinseconds.com/openapi.json).

## List your links

`GET /api/v1/links` returns your links, newest first. Each one has a `status`: `live`, `review` (held by the safety scan), `processing` (a video still being scanned) or `disabled` (turned off by you).

Shell

```
curl -s "https://linkinseconds.com/api/v1/links?limit=5" \
  -H "Authorization: Bearer $LIS_API_KEY"
```

There is also `GET /api/v1/status`, a health check that needs no key. Details are in [List your links](https://linkinseconds.com/docs/api/links).

## Errors you will actually see

Errors come back as JSON with a plain `error` message you can show to a person.

| Status | What it usually means | What to do |

| 401 | Missing, wrong or deleted API key | Check the Authorization header |

| 403 | A plan limit (live links or storage), or an account that can't upload | Read the error and its code |

| 400 | Bad JSON, a blocked file type, a file over your size limit, or a taken link name | Fix the request |

| 409 | That upload path was already used | Start again from step 1 |

| 429 | Too many uploads this hour | Wait, then retry |

| 503 | A temporary problem finishing the upload | Retry step 3 with the same idempotencyKey |

The full list is in [Errors and limits](https://linkinseconds.com/docs/api/errors).

### When a file is held for review

Every file is checked: we read the real size and first bytes from storage, make sure the file is what its name says, and run a safety scan. If the scan flags it, step 3 still returns a link but adds `"review": true`. The link goes live once a person clears it, so tell your users it will start working shortly.

## Limits

The API uses the same plan limits as the website. A few worth knowing up front:

- Per file: 250 MB on Free, 500 MB on Pro, 500 MB on Lifetime.

- Free accounts keep up to 50 live links at a time. Deleting one frees a slot.

- Free accounts using an API key can make 30 uploads per hour. Paid plans keep the normal hourly cap.

- Executables and scripts are always refused. Albums and static websites are created from the website.

Everything else is in [Plans and limits](https://linkinseconds.com/docs/plans-and-limits).

Working inside Claude, Cursor or VS Code? You do not need to write this code at all. Connect the MCP server and the assistant calls the same upload flow for you. See [how to let Claude publish files as links](https://linkinseconds.com/blog/publish-files-from-claude-with-mcp).

## Common questions

### Do the people I send the link to need an account?

No. The link opens in any browser. Only the uploader needs an account and a key.

### Can I upload from a browser app?

Technically yes, but do not ship your key in front-end code. Anyone could read it. Call the API from your server and pass the link to the browser.

### What file types can I upload?

PDFs, images, video, audio, documents, HTML, ZIP and more. The full list is in [Supported file types](https://linkinseconds.com/docs/file-types). For the bigger picture, start at the [developer overview](https://linkinseconds.com/docs/developers) or browse [more developer guides](https://linkinseconds.com/blog/topic/ai-agents-and-developers).

## Keep reading

- [Deploy a React or Vite build dist.zip Live lisview.com/app How to deploy a React or Vite build the easy way 7 min read](https://linkinseconds.com/blog/deploy-a-react-vite-build-the-easy-way)

- [Share an AI-built website as a link site.zip Live lisview.com/site Share a website you built with AI as a live link 4 min read](https://linkinseconds.com/blog/share-an-ai-built-website-as-a-link)

- [Content Signals in robots.txt robots.txt Live lisview.com/robots Content Signals in robots.txt: let AI cite you without training on you 6 min read](https://linkinseconds.com/blog/content-signals-robots-txt)

## Turn any file into a link in seconds

Upload a PDF, image, video, or ZIP and get a clean, trackable link with a QR code, free.

[Try Link in Seconds →](https://linkinseconds.com/)

On this page

- Before you start: get an API key

- Why three calls instead of one?

- Upload with curl

- Upload with Node.js

- Upload with Python

- Make retries safe with an idempotency key

- Useful options

- List your links

- Errors you will actually see

- Limits

- Common questions

[Link in Seconds](https://linkinseconds.com/)

Turn photos, PDFs and files into one clean link to share, with view tracking and a QR code. Works in the browser, nothing to install.

[Follow on LinkedIn](https://www.linkedin.com/company/linkinseconds)[Instagram](https://www.instagram.com/linkinseconds/)[YouTube](https://www.youtube.com/@linkinsecondsapp)

Featured on

33, Bhaktinandan, Sector 4, S G Road, Mota Varachha, Surat, Gujarat 394101, India

[+91 99090 97776](tel:+919909097776)

[support@linkinseconds.com](mailto:support@linkinseconds.com)

Secured by Dodo PaymentsVisaMastercard

Use cases

- [Wedding photos](https://linkinseconds.com/wedding)

- [Event photos](https://linkinseconds.com/event-photo-sharing)

- [Family photos](https://linkinseconds.com/share-photos-with-family)

- [Client proofs](https://linkinseconds.com/client-photo-proofs)

- [Property listings](https://linkinseconds.com/property-listing-link)

- [Product catalogue](https://linkinseconds.com/product-catalogue-link)

- [Pitch decks](https://linkinseconds.com/share-pitch-deck)

- [Resume link](https://linkinseconds.com/resume-link)

- [Portfolio link](https://linkinseconds.com/portfolio-link)

- [Class photos](https://linkinseconds.com/class-photos)

- [All use cases](https://linkinseconds.com/use-cases)

Tools

- [Image to link](https://linkinseconds.com/image-to-link)

- [PDF to link](https://linkinseconds.com/pdf-to-link-converter)

- [Video to link](https://linkinseconds.com/video-to-link)

- [Share files online](https://linkinseconds.com/share-files-online)

- [QR code generator](https://linkinseconds.com/qr-code-generator)

- [Large files by email](https://linkinseconds.com/send-large-files-via-email)

- [View-only sharing](https://linkinseconds.com/view-only-file-sharing)

- [Menu QR code](https://linkinseconds.com/pdf-menu-qr-code)

- [Proposal tracking](https://linkinseconds.com/proposal-tracking)

- [All file viewers](https://linkinseconds.com/viewer)

PDF tools

- [Merge PDF](https://linkinseconds.com/merge-pdf)

- [Split PDF](https://linkinseconds.com/split-pdf)

- [Rotate PDF](https://linkinseconds.com/rotate-pdf)

- [Organise PDF](https://linkinseconds.com/organize-pdf)

- [Watermark PDF](https://linkinseconds.com/watermark-pdf)

- [JPG to PDF](https://linkinseconds.com/jpg-to-pdf)

- [PDF to JPG](https://linkinseconds.com/pdf-to-jpg)

- [All PDF tools](https://linkinseconds.com/pdf-tools)

Hosting

- [Host a static site](https://linkinseconds.com/host-static-website)

- [HTML hosting](https://linkinseconds.com/html-hosting)

- [Publish an HTML ZIP](https://linkinseconds.com/publish-html-zip)

- [Host AI-generated HTML](https://linkinseconds.com/host-ai-generated-html)

- [Deploy a Vite build](https://linkinseconds.com/deploy-vite-react-build)

- [Publish from AI (MCP)](https://linkinseconds.com/docs/mcp)

- [File upload API](https://linkinseconds.com/docs/api/uploads)

Compare

- [vs WeTransfer](https://linkinseconds.com/wetransfer-alternative)

- [vs Dropbox](https://linkinseconds.com/dropbox-alternative)

- [vs Google Drive](https://linkinseconds.com/google-drive-alternative)

- [vs Tiiny Host](https://linkinseconds.com/tiiny-host-alternative)

- [vs DocSend](https://linkinseconds.com/docsend-alternative)

- [vs Google Photos](https://linkinseconds.com/google-photos-alternative)

- [vs Pixieset](https://linkinseconds.com/pixieset-alternative)

- [vs Netlify Drop](https://linkinseconds.com/netlify-drop-alternative)

- [vs Smash](https://linkinseconds.com/smash-alternative)

- [vs Issuu](https://linkinseconds.com/issuu-alternative)

Company

- [Pricing](https://linkinseconds.com/pricing)

- [Blog](https://linkinseconds.com/blog)

- [FAQ](https://linkinseconds.com/faq)

- [About](https://linkinseconds.com/about)

- [File sharing stats](https://linkinseconds.com/file-sharing-statistics)

- [Contact](https://linkinseconds.com/contact)

- [Contact sales](https://linkinseconds.com/contact-sales)

- [Docs](https://linkinseconds.com/docs)

- [API & MCP](https://linkinseconds.com/docs/developers)

Legal

- [Terms](https://linkinseconds.com/terms)

- [Privacy](https://linkinseconds.com/privacy)

- [Cookies](https://linkinseconds.com/cookies)

- [Refunds](https://linkinseconds.com/refunds)

- [Acceptable use](https://linkinseconds.com/acceptable-use)

- [Copyright](https://linkinseconds.com/dmca)

- [Report abuse](https://linkinseconds.com/abuse)

- [Child safety](https://linkinseconds.com/child-safety)

- [Grievance](https://linkinseconds.com/grievance)

- [Trust and safety](https://linkinseconds.com/trust)

- [Security](https://linkinseconds.com/security)

- [Legal notice](https://linkinseconds.com/legal-notice)

© 2026 Link in Seconds. All rights reserved.

[Terms](https://linkinseconds.com/terms)[Privacy](https://linkinseconds.com/privacy)[Cookies](https://linkinseconds.com/cookies)

Source: https://linkinseconds.com/blog/upload-a-file-by-api
