# Storage and blobs

Private files with signed transfers, and public objects.

> Section: Storage

## Permissions

blob.read permits reads, blob.write permits uploads and deletion. The API key must belong to the project that owns the file, and it must stay on your server.

## Upload flow

Request a ticket, PUT the file directly to the returned URL with the upload token, then confirm. Upload tickets are single-use; download tickets stay valid until they expire.

```typescript
const ticket = await fetch(`${base}/api/storage/upload-ticket`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${process.env.VELTIC_API_KEY}`
  },
  body: JSON.stringify({ projectId, key: 'invoices/2026-09.pdf' })
}).then(response => response.json());

await fetch(ticket.uploadUrl, {
  method: 'PUT',
  headers: { 'x-veltic-upload-token': ticket.token },
  body: file
});

await fetch(`${base}/api/storage/confirm`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.VELTIC_API_KEY}` },
  body: JSON.stringify({ ticketId: ticket.id })
});
```

## Downloads

The download ticket endpoint returns a time-limited URL that supports range requests, so media players and resumable downloads work. Your API key is never appended to that link. Every endpoint returns an expiry; use it rather than caching a URL indefinitely.

## Public blobs

Blobs are reachable without authentication, which is the point. Freezing a blob URL into the database goes through the canonical production origin rather than a preview host, so a stored link cannot break when a preview environment is torn down.
