# Quickstart

Upload a file, ask for an operation, receive the result. Three calls, none of which block on the work.

Fylane never moves file bytes through the API. You ask for a place to put a file, you upload it directly to that place, and you ask for work to be done on it. That is the whole shape, and it is why a 2 GB file costs the same to accept as a 2 KB one.

> **Signup is not open yet**
>
> The API described here is being built in the open. These calls are the committed contract, not a live endpoint — the OpenAPI document at /openapi.json is the machine-readable version of the same thing.

## One call, with the SDK

The Node SDK owns all three steps — the upload dance, the job, the polling — and returns the finished job. Everything below remains true and is exactly what the SDK does on your behalf.

The whole loop in one call

```typescript
import { Fylane } from '@fylane/node';

const fylane = new Fylane(process.env.FYLANE_API_KEY);

const job = await fylane.files.process('./report.docx', { operation: 'convert' });
console.log(job.state, job.artifacts);
```

> **A failed job is a result, not an exception**
>
> The SDK returns a job in state `failed` with the reason on `job.error`. Only transport and request errors throw — as `FylaneApiError`, carrying the same code, message and request id the API sent.

## The three steps

1. `POST /v1/uploads` returns a pre-signed URL that is single-use, size-capped and short-lived.
2. `PUT` the bytes straight to that URL. Nothing about this step touches Fylane compute.
3. `POST /v1/jobs` asks for an operation. It returns `202` and a job, not a result.

The third step returns immediately because anything slower than roughly three seconds is asynchronous by design. Subscribe a webhook or poll the job; both are documented below.

## Create an upload session

```bash
curl -X POST https://api.fylane.dev/v1/uploads \
  -H "Authorization: Bearer fy_test_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "filename": "report.pdf", "byte_size": 482113, "mime_type": "application/pdf" }'
```

Response — `201 Created`

```json
{
  "id": "ups_01JBQ8Z5T7WXK9MNP2RSTVA3C4",
  "object": "upload_session",
  "file_id": "file_01JBQ8Z5T7WXK9MNP2RSTVA3C5",
  "url": "https://uploads.fylane.dev/...",
  "method": "PUT",
  "max_bytes": 482113,
  "status": "pending",
  "expires_at": "2026-08-20T15:03:11Z"
}
```

> **Treat the upload URL as a secret**
>
> It is a capability. Anyone holding it can write those exact bytes to that exact object until it expires. Do not log it, and do not pass it to a browser you did not mint it for.

## Upload the bytes

```bash
curl -X PUT "$UPLOAD_URL" \
  --upload-file report.pdf \
  -H "Content-Type: application/pdf"
```

The signature enforces `max_bytes`. A larger body is refused by storage rather than by us, which is the point — an oversized upload never reaches anything that could be made to do work.

## Ask for work

```bash
curl -X POST https://api.fylane.dev/v1/jobs \
  -H "Authorization: Bearer fy_test_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "file_id": "file_01JBQ8Z5T7WXK9MNP2RSTVA3C5", "preset": "ai-ready" }'
```

TypeScript, without the SDK

```typescript
const upload = await fetch('https://api.fylane.dev/v1/uploads', {
  method: 'POST',
  headers: {
    authorization: `Bearer ${process.env.FYLANE_KEY}`,
    'idempotency-key': crypto.randomUUID(),
    'content-type': 'application/json',
  },
  body: JSON.stringify({ filename: 'report.pdf', byte_size: bytes.byteLength }),
}).then((response) => response.json());

await fetch(upload.url, { method: 'PUT', body: bytes });

const job = await fetch('https://api.fylane.dev/v1/jobs', {
  method: 'POST',
  headers: {
    authorization: `Bearer ${process.env.FYLANE_KEY}`,
    'idempotency-key': crypto.randomUUID(),
    'content-type': 'application/json',
  },
  body: JSON.stringify({ file_id: upload.file_id, preset: 'ai-ready' }),
}).then((response) => response.json());

// job.state === 'queued'. Subscribe a webhook rather than polling in a loop.
```

Python, without the SDK

```python
import os, uuid, httpx

headers = {
    "authorization": f"Bearer {os.environ['FYLANE_KEY']}",
    "idempotency-key": str(uuid.uuid4()),
}

upload = httpx.post(
    "https://api.fylane.dev/v1/uploads",
    headers=headers,
    json={"filename": "report.pdf", "byte_size": len(data)},
).json()

httpx.put(upload["url"], content=data)

job = httpx.post(
    "https://api.fylane.dev/v1/jobs",
    headers={**headers, "idempotency-key": str(uuid.uuid4())},
    json={"file_id": upload["file_id"], "preset": "ai-ready"},
).json()
```

## Getting the result

Register a webhook endpoint and let Fylane tell you. Polling works and is documented, but a poll loop against a job that takes four minutes is four minutes of requests that all say "not yet".

| Approach | Use when | Cost |
| --- | --- | --- |
| Webhook | Anything in production | One signed request per event |
| Poll `GET /v1/jobs/{id}` | Scripts, local development | One request per poll, rate limited like any other |

## Every mutating call takes an idempotency key

A replay with the same body returns the stored response without re-executing and without re-billing. A replay with a different body returns `409 idempotency_conflict`. Keys are scoped to your organisation and the endpoint, and retained for 24 hours.

> **Retries are your responsibility to make safe, and ours to honour**
>
> A network timeout does not tell you whether the request arrived. Retrying with the same idempotency key is always safe; retrying without one may cost you twice.
