API reference
Store, list, move, share and serve files from your own program. Everything here is made from the API's own definition of itself, so it is what the API does.
Endpoints on this page
- Getting started
Files and folders
Uploading
Downloading
Usage
- Refusals
Every address below begins with https://api.filefreak.io.
Send the key on every request: Authorization: Bearer ffk_live_... A key is a password for the app. Keep it on a server, never in a web page or an app somebody can take apart.
Make an app and a key in Developers. A key is given some of these scopes, and can be kept to one folder of its app: files:read, files:write, files:delete, usage:read.
A file's bytes never pass through the API. To upload, the API gives addresses and the bytes go straight to storage; to download, it gives an address on storage. That is why a file of any size can be sent by anything that can make a PUT request, including a browser that has never seen your key.
Where an endpoint says it honours one, send an Idempotency-Key header, any string of your own that is unique to the thing you are trying to do. The same request sent again with the same key is given the first answer and makes nothing twice. Keys are remembered for a day.
One key may make 600 calls a minute. Images can be asked for at 64, 128, 256, 512, 1024 pixels.
Files and folders
List a folder
GET/v1/files
Needs the scope files:read.
What is in a folder, a page at a time: folders first, then files, each by name. Pass nextCursor back as cursor for the next page; when it is null there is no more. Things in the trash are not listed. A page never repeats or skips something because of what was added or removed between two requests.
In the query
parent | id | The folder to list. Left out, the top of the app (or of the folder the key is confined to). |
|---|---|---|
cursor | string | nextCursor from the page before. |
limit | integer | How many to return, 1 to 200. Left out: 50. |
200 Always.
Answers with
itemsrequired | list of object | |
|---|---|---|
items[].idrequired | id | Never changes, whatever is renamed or moved. |
items[].kindrequired | one of: file, folder | |
items[].namerequired | string | |
items[].parentIdrequired | id, or null | The folder it is in. Null at the top of the app. |
items[].sizeBytesrequired | integer | Zero for a folder. |
items[].contentTyperequired | string, or null | Null for a folder. |
items[].visibilityrequired | one of: private, public | Its own setting. A private file inside a public folder is still public. |
items[].publicUrlrequired | string, or null | An address anybody can fetch, that does not expire, while the file is public. Null for a folder and for anything that is not public. |
items[].trashedAtrequired | string, or null | When it was put in the trash, if it is there. |
items[].createdAtrequired | string | |
items[].updatedAtrequired | string | |
nextCursorrequired | string, or null |
One file or folder
GET/v1/files/:id
Needs the scope files:read.
By its id. Something in the trash is still returned, with trashedAt set.
In the address
idrequired | id |
|---|
200 It exists and the key can reach it.
Answers with
itemrequired | object | |
|---|---|---|
item.idrequired | id | Never changes, whatever is renamed or moved. |
item.kindrequired | one of: file, folder | |
item.namerequired | string | |
item.parentIdrequired | id, or null | The folder it is in. Null at the top of the app. |
item.sizeBytesrequired | integer | Zero for a folder. |
item.contentTyperequired | string, or null | Null for a folder. |
item.visibilityrequired | one of: private, public | Its own setting. A private file inside a public folder is still public. |
item.publicUrlrequired | string, or null | An address anybody can fetch, that does not expire, while the file is public. Null for a folder and for anything that is not public. |
item.trashedAtrequired | string, or null | When it was put in the trash, if it is there. |
item.createdAtrequired | string | |
item.updatedAtrequired | string |
Rename, move, or make public
PATCH/v1/files/:id
Needs the scope files:write.
Any of the three, in one request. They are applied in that order, each as its own change: if the second is refused the first has still happened, and the answer says which was refused. Moving a folder moves everything in it and is one change however much that is. Making a folder public makes everything in it public.
In the address
idrequired | id |
|---|
In the body, as JSON
name | string | A new name. Refused with name_taken if the folder already has something called that. |
|---|---|---|
parentId | id, or null | The folder to move it into, or null for the top of the app. |
visibility | one of: private, public |
200 Every change asked for was made.
Answers with
itemrequired | object | |
|---|---|---|
item.idrequired | id | Never changes, whatever is renamed or moved. |
item.kindrequired | one of: file, folder | |
item.namerequired | string | |
item.parentIdrequired | id, or null | The folder it is in. Null at the top of the app. |
item.sizeBytesrequired | integer | Zero for a folder. |
item.contentTyperequired | string, or null | Null for a folder. |
item.visibilityrequired | one of: private, public | Its own setting. A private file inside a public folder is still public. |
item.publicUrlrequired | string, or null | An address anybody can fetch, that does not expire, while the file is public. Null for a folder and for anything that is not public. |
item.trashedAtrequired | string, or null | When it was put in the trash, if it is there. |
item.createdAtrequired | string | |
item.updatedAtrequired | string |
Put in the trash, or delete for good
DELETE/v1/files/:id
Needs the scope files:delete.
Without permanent, the file or folder goes to the trash: it can be restored, it still counts toward storage, and it is removed for good after thirty days. With permanent=true it is removed now, with everything inside it, and cannot be brought back.
In the address
idrequired | id |
|---|
In the query
permanent | one of: true, false | true to delete for good instead of putting in the trash. Left out: "false". |
|---|
200 It is in the trash, or gone.
Answers with
okrequired | true |
|---|
Take out of the trash
POST/v1/files/:id/restore
Needs the scope files:write.
It goes back where it was. If that folder has gone it goes to the top, and if its name has been taken in the meantime it comes back with a number after it.
In the address
idrequired | id |
|---|
200 It is out of the trash.
Answers with
itemrequired | object | |
|---|---|---|
item.idrequired | id | Never changes, whatever is renamed or moved. |
item.kindrequired | one of: file, folder | |
item.namerequired | string | |
item.parentIdrequired | id, or null | The folder it is in. Null at the top of the app. |
item.sizeBytesrequired | integer | Zero for a folder. |
item.contentTyperequired | string, or null | Null for a folder. |
item.visibilityrequired | one of: private, public | Its own setting. A private file inside a public folder is still public. |
item.publicUrlrequired | string, or null | An address anybody can fetch, that does not expire, while the file is public. Null for a folder and for anything that is not public. |
item.trashedAtrequired | string, or null | When it was put in the trash, if it is there. |
item.createdAtrequired | string | |
item.updatedAtrequired | string |
Make a folder
POST/v1/folders
Needs the scope files:write. Honours an Idempotency-Key.
Folders can be nested up to 32 deep. With ifExists=use, a folder of that name that is already there is returned and nothing is made, which is what a program that makes its folders on every start wants.
In the body, as JSON
namerequired | string | |
|---|---|---|
parentId | id, or null | The folder to make it in. Null, or left out, for the top of the app. Left out: null. |
ifExists | one of: fail, use | What to do if the folder already has one of that name. Left out: "fail". |
201 A folder was made.
Answers with
itemrequired | object | |
|---|---|---|
item.idrequired | id | Never changes, whatever is renamed or moved. |
item.kindrequired | one of: file, folder | |
item.namerequired | string | |
item.parentIdrequired | id, or null | The folder it is in. Null at the top of the app. |
item.sizeBytesrequired | integer | Zero for a folder. |
item.contentTyperequired | string, or null | Null for a folder. |
item.visibilityrequired | one of: private, public | Its own setting. A private file inside a public folder is still public. |
item.publicUrlrequired | string, or null | An address anybody can fetch, that does not expire, while the file is public. Null for a folder and for anything that is not public. |
item.trashedAtrequired | string, or null | When it was put in the trash, if it is there. |
item.createdAtrequired | string | |
item.updatedAtrequired | string |
200 ifExists was use, and one was already there.
Answers with
itemrequired | object | |
|---|---|---|
item.idrequired | id | Never changes, whatever is renamed or moved. |
item.kindrequired | one of: file, folder | |
item.namerequired | string | |
item.parentIdrequired | id, or null | The folder it is in. Null at the top of the app. |
item.sizeBytesrequired | integer | Zero for a folder. |
item.contentTyperequired | string, or null | Null for a folder. |
item.visibilityrequired | one of: private, public | Its own setting. A private file inside a public folder is still public. |
item.publicUrlrequired | string, or null | An address anybody can fetch, that does not expire, while the file is public. Null for a folder and for anything that is not public. |
item.trashedAtrequired | string, or null | When it was put in the trash, if it is there. |
item.createdAtrequired | string | |
item.updatedAtrequired | string |
Uploading
Begin an upload
POST/v1/uploads
Needs the scope files:write. Honours an Idempotency-Key.
Says what is about to be sent, and answers with where to send it. A file's bytes never pass through this API: they go straight to storage, in parts, each with a plain PUT to the address given for it. The first addresses come back here; ask for the rest as they are needed. Then complete the upload, and it becomes a file.
The room is set aside now, so an upload that would not fit is refused before a byte is sent. The parts can be sent by anything that can make a PUT request, including a browser that has never seen the API key: hand it the addresses.
In the body, as JSON
namerequired | string | |
|---|---|---|
sizeBytesrequired | integer | Exactly how many bytes will be sent. What arrives is measured, and that is what is counted. |
contentType | string | Left out, it is worked out from the name. |
parentId | id, or null | The folder to put it in. Null, or left out, for the top of the app. Left out: null. |
onConflict | one of: rename, replace, fail | What to do if the folder already has a file of that name. replace keeps the file's id and its public address. Left out: "rename". |
imageSizes | list of integer | Sizes to make of it, if it is an image, so they are ready when wanted: any of 64, 128, 256, 512, 1024. |
201 The upload has begun.
Answers with
uploadrequired | object | |
|---|---|---|
upload.idrequired | id | |
upload.namerequired | string | |
upload.parentIdrequired | id, or null | |
upload.sizeBytesrequired | integer | |
upload.contentTyperequired | string, or null | |
upload.statusrequired | one of: pending, completed, aborted, expired | |
upload.onConflictrequired | one of: rename, replace, fail | |
upload.partBytesrequired | integer | Every part is this many bytes, except the last, which is whatever remains. |
upload.partCountrequired | integer | |
upload.fileIdrequired | id, or null | The file this became, once it has. |
upload.expiresAtrequired | string | The upload has to be completed by then. |
upload.createdAtrequired | string | |
partsrequired | list of object | |
parts[].numberrequired | integer | Counted from zero. |
parts[].methodrequired | "PUT" | |
parts[].urlrequired | string | Send this part's bytes here, as the body. No API key: the address is the permission. |
parts[].headersrequired | value | Headers the request must carry. |
parts[].expiresAtrequired | string | When this address stops working. Ask for the parts again to get fresh ones. |
An upload, and which parts have arrived
GET/v1/uploads/:id
Needs the scope files:write.
For carrying on after an interruption: storage itself says which parts it has, so nothing the client remembered is needed.
In the address
idrequired | id |
|---|
200 Always.
Answers with
uploadrequired | object | |
|---|---|---|
upload.idrequired | id | |
upload.namerequired | string | |
upload.parentIdrequired | id, or null | |
upload.sizeBytesrequired | integer | |
upload.contentTyperequired | string, or null | |
upload.statusrequired | one of: pending, completed, aborted, expired | |
upload.onConflictrequired | one of: rename, replace, fail | |
upload.partBytesrequired | integer | Every part is this many bytes, except the last, which is whatever remains. |
upload.partCountrequired | integer | |
upload.fileIdrequired | id, or null | The file this became, once it has. |
upload.expiresAtrequired | string | The upload has to be completed by then. |
upload.createdAtrequired | string | |
uploadedPartsrequired | list of integer | The numbers of the parts storage has. |
Addresses for more parts
GET/v1/uploads/:id/parts
Needs the scope files:write.
Addresses are given out a window at a time and each lasts half an hour. Ask again for any that have run out.
In the address
idrequired | id |
|---|
In the query
from | integer | The first part wanted, counted from zero. Left out: 0. |
|---|---|---|
count | integer | Left out: 16. |
200 Always.
Answers with
partsrequired | list of object | |
|---|---|---|
parts[].numberrequired | integer | Counted from zero. |
parts[].methodrequired | "PUT" | |
parts[].urlrequired | string | Send this part's bytes here, as the body. No API key: the address is the permission. |
parts[].headersrequired | value | Headers the request must carry. |
parts[].expiresAtrequired | string | When this address stops working. Ask for the parts again to get fresh ones. |
Finish an upload
POST/v1/uploads/:id/complete
Needs the scope files:write.
Turns the parts into a file. Safe to call again: a second call for an upload that has finished answers with the same file. For a very large file storage may still be putting it together, and the answer is then 202 with how long to wait before asking again.
In the address
idrequired | id |
|---|
200 The file exists.
Answers with
itemrequired | object | |
|---|---|---|
item.idrequired | id | Never changes, whatever is renamed or moved. |
item.kindrequired | one of: file, folder | |
item.namerequired | string | |
item.parentIdrequired | id, or null | The folder it is in. Null at the top of the app. |
item.sizeBytesrequired | integer | Zero for a folder. |
item.contentTyperequired | string, or null | Null for a folder. |
item.visibilityrequired | one of: private, public | Its own setting. A private file inside a public folder is still public. |
item.publicUrlrequired | string, or null | An address anybody can fetch, that does not expire, while the file is public. Null for a folder and for anything that is not public. |
item.trashedAtrequired | string, or null | When it was put in the trash, if it is there. |
item.createdAtrequired | string | |
item.updatedAtrequired | string |
202 Storage is still putting it together. Ask again after retryAfterSeconds.
Answers with
pendingrequired | true | |
|---|---|---|
retryAfterSecondsrequired | integer |
Cancel an upload
DELETE/v1/uploads/:id
Needs the scope files:write.
Whatever had arrived is deleted, and the room it held comes back at once.
In the address
idrequired | id |
|---|
200 It is cancelled.
Answers with
okrequired | true |
|---|
Downloading
An address to download a file from
GET/v1/files/:id/download
Needs the scope files:read.
A short-lived address on storage itself. Anybody holding it can fetch the file until it expires, with no key, so it can be handed to a browser. Each one issued counts the file's size toward the month's downloads. For an address that does not expire, make the file public and use its publicUrl.
In the address
idrequired | id |
|---|
In the query
inline | one of: true, false | true to have a browser show it, for the types that are safe to show. Anything else is saved, whatever is asked. Left out: "false". |
|---|
200 Always.
Answers with
urlrequired | string | |
|---|---|---|
expiresAtrequired | string | |
inlinerequired | true or false | Whether the address will show the file or save it. |
contentTyperequired | string, or null |
An image at a smaller size
GET/v1/files/:id/image
Needs the scope files:read.
A copy of an image no more than size pixels on its longest side, as WebP, with the camera's metadata removed. The sizes are 64, 128, 256, 512, 1024 and no others. Each is made once, the first time it is asked for, and kept: until it has been made the answer is 202. Name the sizes when the image is uploaded and they are ready by the time they are wanted. For a public file, add ?size= to its publicUrl.
In the address
idrequired | id |
|---|
In the query
sizerequired | integer | One of 64, 128, 256, 512, 1024. |
|---|
200 That size has been made.
Answers with
statusrequired | "ready" | |
|---|---|---|
urlrequired | string | |
widthrequired | integer | |
heightrequired | integer | |
expiresAtrequired | string |
202 It is being made. Ask again after retryAfterSeconds.
Answers with
statusrequired | "pending" | |
|---|---|---|
retryAfterSecondsrequired | integer |
Usage
What the app is using
GET/v1/usage
Needs the scope usage:read.
What this app holds, against the ceiling set for it if there is one; and what the account it belongs to has used this calendar month, across its personal files and all its apps, against what its plan includes. An app's files count toward the account's one storage allowance.
200 Always.
Answers with
apprequired | object | |
|---|---|---|
app.storedBytesrequired | integer | |
app.fileCountrequired | integer | |
app.storageCapBytesrequired | integer, or null | The ceiling set for this app, or null for none. |
app.apiCallsThisMonthrequired | integer | |
accountrequired | object | |
account.planrequired | string | |
account.storageUsedBytesrequired | integer | |
account.storageIncludedBytesrequired | integer | |
account.storageLimitBytesrequired | integer | Where uploads are refused: what the plan includes, plus what the account's spending limit allows beyond it. |
account.downloadedBytesThisMonthrequired | integer | |
account.downloadsIncludedBytesrequired | integer | |
account.apiCallsThisMonthrequired | integer | |
account.apiCallsIncludedrequired | integer |
Refusals
Every refusal is JSON: { error, message }. `error` is a short code that does not change, and is what a program should branch on. `message` is a sentence for a person, and may be reworded.
| Status | error | What it means |
|---|---|---|
| 400 | invalid_request | Something sent was missing or the wrong shape. `field` names it. |
| 400 | invalid_cursor | The cursor is not one this API gave out. |
| 400 | unknown_size | An image was asked for at a size that is not on the list. `sizes` is the list. |
| 400 | size_mismatch | What arrived for an upload is not the size that was announced. Nothing was kept. |
| 400 | not_a_folder | A file was named where a folder was wanted. |
| 401 | missing_key | No API key was sent. |
| 401 | invalid_key | The key is not one this service made. |
| 401 | key_revoked | The key has been revoked. |
| 401 | key_expired | The key has passed its expiry. |
| 402 | plan_required | The account is not on Pro. The developer API is part of Pro. |
| 402 | plan_ended | Pro has ended for the account. Its keys can only read, until readOnlyUntil. |
| 402 | spending_limit | Beyond what the plan includes, and paying for it would pass the account's spending limit. |
| 402 | download_limit | The month's download allowance is used. |
| 403 | insufficient_scope | The key is not allowed to do that. |
| 403 | confined_key | The key is for one folder, and that is the folder itself or covers the whole app. |
| 403 | app_disabled | The app is switched off. |
| 403 | account_suspended | The account the key belongs to is suspended. |
| 404 | not_found | There is no such file, folder or upload that this key can reach. |
| 409 | name_taken | The folder already has something of that name. |
| 409 | request_in_progress | A request with the same Idempotency-Key is still being answered. |
| 409 | upload_completed | The upload has already become a file, and cannot be cancelled. |
| 410 | upload_aborted | The upload was cancelled. |
| 410 | upload_expired | The upload was not completed in time. |
| 413 | quota_exceeded | It would take the account past what it may store. |
| 413 | app_quota_exceeded | It would take the app past the ceiling set for it. |
| 422 | idempotency_key_reused | That Idempotency-Key was used for a different request. |
| 422 | not_an_image | The file is not an image that can be resized. |
| 429 | rate_limited | More than 600 calls from one key in a minute. Retry-After says how long to wait. |