Files

Project files and external links. Numeric ids; the flat delete address takes a fil- ref.

Base URL https://app.fareeqy.com/api/v1

The resource

The fields returned inside data by the endpoints in this group.

idinteger

Numeric id (files have no slug).

namestring
pathstring

The folder path the file lives at.

folder_idinteger | null
content_typestring | null
size_bytesinteger | null
size_humanstring | null
external_urlstring | null

Set for an external link, null for an upload.

sourcestring | null

The source's display name.

scan_statestring

Virus-scan state (e.g. pending, clean).

downloadableboolean

True once the scan has cleared the file.

uploaded_bystring | null
created_atstring (date-time)
ProjectFile
{
  "id": 108,
  "name": "الهوية-البصرية.pdf",
  "path": "التصاميم/الهوية-البصرية.pdf",
  "folder_id": 42,
  "content_type": "application/pdf",
  "size_bytes": 2418176,
  "size_human": "2.31 م.بايت",
  "external_url": null,
  "source": null,
  "scan_state": "clean",
  "downloadable": true,
  "uploaded_by": "سارة العتيبي",
  "created_at": "2026-07-10T16:31:44.000+03:00"
}

List one level of a project's file browser

GET/projects/{project_id}/filesRequires files:read

The folders and files directly inside folder_id (or the project root). Not recursive — walk down by passing a subfolder's id back. Files are paginated; folders are not.

Path parameters

project_idstringrequired

The project REF, or its slug.

Query parameters

folder_idinteger

The folder to list inside; omit for the project root.

limitinteger

Page size (default 50, max 100, clamped).

Default: 50

offsetinteger

Number of records to skip (default 0).

Default: 0

Request

cURL
curl "https://app.fareeqy.com/api/v1/projects/prj-8f3kd/files" \
  -H "Authorization: Bearer $FAREEQY_API_KEY"

Response

200
{
  "data": {
    "folder": {
      "id": 42,
      "name": "التصاميم",
      "path": "التصاميم",
      "description": "ملفات الهوية وواجهات الموقع.",
      "parent_folder_id": null,
      "created_at": "2026-07-05T08:20:00.000+03:00"
    },
    "folders": [
      {
        "id": 51,
        "name": "الأيقونات",
        "path": "التصاميم/الأيقونات",
        "description": null,
        "parent_folder_id": 42,
        "created_at": "2026-07-06T09:02:14.000+03:00"
      }
    ],
    "files": [
      {
        "id": 108,
        "name": "الهوية-البصرية.pdf",
        "path": "التصاميم/الهوية-البصرية.pdf",
        "folder_id": 42,
        "content_type": "application/pdf",
        "size_bytes": 2418176,
        "size_human": "2.31 م.بايت",
        "external_url": null,
        "source": null,
        "scan_state": "clean",
        "downloadable": true,
        "uploaded_by": "سارة العتيبي",
        "created_at": "2026-07-10T16:31:44.000+03:00"
      },
      {
        "id": 131,
        "name": "واجهات الصفحة الرئيسية",
        "path": "التصاميم/واجهات الصفحة الرئيسية",
        "folder_id": 42,
        "content_type": null,
        "size_bytes": null,
        "size_human": null,
        "external_url": "https://www.figma.com/design/9aZq/home-v3",
        "source": "Figma",
        "scan_state": "skipped",
        "downloadable": true,
        "uploaded_by": "سارة العتيبي",
        "created_at": "2026-07-12T11:04:20.000+03:00"
      },
      {
        "id": 147,
        "name": "لقطة-الصفحة-الرئيسية.png",
        "path": "التصاميم/لقطة-الصفحة-الرئيسية.png",
        "folder_id": 42,
        "content_type": "image/png",
        "size_bytes": 884736,
        "size_human": "864 ك.بايت",
        "external_url": null,
        "source": null,
        "scan_state": "clean",
        "downloadable": true,
        "uploaded_by": "عبدالله المطيري",
        "created_at": "2026-07-14T10:22:07.000+03:00"
      },
      {
        "id": 152,
        "name": "عرض-التدفق-الجديد.mp4",
        "path": "التصاميم/عرض-التدفق-الجديد.mp4",
        "folder_id": 42,
        "content_type": "video/mp4",
        "size_bytes": 41943040,
        "size_human": "40 م.بايت",
        "external_url": null,
        "source": null,
        "scan_state": "pending",
        "downloadable": false,
        "uploaded_by": "نورة الحربي",
        "created_at": "2026-07-15T13:47:58.000+03:00"
      }
    ]
  },
  "meta": {
    "total": 4,
    "limit": 50,
    "offset": 0
  }
}
Refusals:401403404429Show failure examples
401default

Missing or invalid API key.

default
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid or missing API key."
  }
}
403forbidden

Two different refusals share this status, and a client must tell them apart by error.code. forbidden means the key's scope or allowlist does not permit this operation, or Pundit denied the action. plan_upgrade_required means the company's plan carries no API access at all, so no key on it can ever succeed and there is nothing to retry.

This key may not perform this operation
{
  "error": {
    "code": "forbidden",
    "message": "This API key is not permitted to perform this operation."
  }
}
The company's plan carries no API access
{
  "error": {
    "code": "plan_upgrade_required",
    "message": "خطة «الاحترافي» لا تشمل الوصول إلى API. رقِّ إلى «المتطور» أو «الإنتاجي» لتفعيله. — The الاحترافي plan does not include API access. Upgrade to «المتطور» or «الإنتاجي» to enable it."
  }
}
404default

Resource not found or not accessible — also returned for EVERY endpoint when the company's rest_api feature flag is disabled (the surface is hidden). Lookups drill through the URL hierarchy, so another company's record is a 404 and never a leak. A path that matches no route at all answers 404 with the distinct code unknown_endpoint and echoes the path back, so a mistyped or half-built URL is told apart from a record that is missing or out of reach.

default
{
  "error": {
    "code": "not_found",
    "message": "Resource not found, or you do not have access to it."
  }
}
429rate_limit_exceeded

Either the company's daily API allowance is spent (rate_limit_exceeded), or the per-key / per-IP burst throttle of 300 requests per minute fired. Both come back after a wait, so Retry-After is honest here. The two bodies are not the same shape. The daily-quota refusal uses the standard error envelope. The burst throttle is served by Rack::Attack ahead of the application, so its body is a flat {"error": "<string>"} with no code. A client that reads error.code has to tolerate error being a plain string.

Today's daily allowance is spent (application envelope)
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "استهلكت رصيد اليوم من طلبات API في خطة «المتطور» (1000 طلب يوميًا). يتجدد الرصيد عند منتصف الليل بتوقيت Asia/Riyadh. — Daily API quota exhausted: the المتطور plan allows 1000 calls per day. It resets at midnight Asia/Riyadh."
  }
}
Over 300 requests in a minute (Rack::Attack body, flat error)
{
  "error": "Rate limit exceeded. Please try again later."
}
Errors

Upload a file from inline content

POST/projects/{project_id}/filesRequires files:write

Uploads a file from inline content (UTF-8 text) OR content_base64 (binary, standard base64, no line breaks) — exactly one, not both. Capped at 5 MB decoded. A freshly uploaded file is pending (not downloadable) until the virus scan clears it. `content` and the file name have to agree. The name is a claim about what the bytes are, so content is accepted only for a plain-text extension — documents (md, txt, rst, tex), data and config (csv, json, yml, xml, toml, ini, conf, env), markup and styles (html, svg, css, scss), source (js, ts, rb, py, sh, sql, graphql) and the rest of that kind — or a name with no extension at all. A .pdf name is the one exception: the content is read as markdown and rendered into a real PDF document, capped at 256 KB of markdown because the render holds a request thread for its whole duration. Any other extension is refused with a 422 rather than stored, because text under a container extension such as .docx produces a file no viewer can open. Send genuine bytes as content_base64 instead.

Path parameters

project_idstringrequired

The project REF, or its slug.

Request body

namestringrequired

The file name (with extension).

folder_idinteger | null

Destination folder; omit for the project root.

contentstring | null

UTF-8 text content. Provide this OR content_base64.

content_base64string | null

Base64-encoded bytes. Provide this OR content.

Request

cURL
curl -X POST "https://app.fareeqy.com/api/v1/projects/prj-8f3kd/files" \
  -H "Authorization: Bearer $FAREEQY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"","folder_id":null,"content":null,"content_base64":null}'

Response

201
{
  "data": {
    "id": 108,
    "name": "الهوية-البصرية.pdf",
    "path": "التصاميم/الهوية-البصرية.pdf",
    "folder_id": 42,
    "content_type": "application/pdf",
    "size_bytes": 2418176,
    "size_human": "2.31 م.بايت",
    "external_url": null,
    "source": null,
    "scan_state": "clean",
    "downloadable": true,
    "uploaded_by": "سارة العتيبي",
    "created_at": "2026-07-10T16:31:44.000+03:00"
  }
}
Refusals:401403404409422429Show failure examples
401default

Missing or invalid API key.

default
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid or missing API key."
  }
}
403forbidden

Two different refusals share this status, and a client must tell them apart by error.code. forbidden means the key's scope or allowlist does not permit this operation, or Pundit denied the action. plan_upgrade_required means the company's plan carries no API access at all, so no key on it can ever succeed and there is nothing to retry.

This key may not perform this operation
{
  "error": {
    "code": "forbidden",
    "message": "This API key is not permitted to perform this operation."
  }
}
The company's plan carries no API access
{
  "error": {
    "code": "plan_upgrade_required",
    "message": "خطة «الاحترافي» لا تشمل الوصول إلى API. رقِّ إلى «المتطور» أو «الإنتاجي» لتفعيله. — The الاحترافي plan does not include API access. Upgrade to «المتطور» or «الإنتاجي» to enable it."
  }
}
404default

Resource not found or not accessible — also returned for EVERY endpoint when the company's rest_api feature flag is disabled (the surface is hidden). Lookups drill through the URL hierarchy, so another company's record is a 404 and never a leak. A path that matches no route at all answers 404 with the distinct code unknown_endpoint and echoes the path back, so a mistyped or half-built URL is told apart from a record that is missing or out of reach.

default
{
  "error": {
    "code": "not_found",
    "message": "Resource not found, or you do not have access to it."
  }
}
409default

A uniqueness/record conflict; retry.

default
{
  "error": {
    "code": "conflict",
    "message": "Could not complete due to a conflict; please retry."
  }
}
422default

A caller-fixable bad request (validation error, bad date, bad enum).

default
{
  "error": {
    "code": "unprocessable_entity",
    "message": "Title can't be blank"
  }
}
429rate_limit_exceeded

Either the company's daily API allowance is spent (rate_limit_exceeded), or the per-key / per-IP burst throttle of 300 requests per minute fired. Both come back after a wait, so Retry-After is honest here. The two bodies are not the same shape. The daily-quota refusal uses the standard error envelope. The burst throttle is served by Rack::Attack ahead of the application, so its body is a flat {"error": "<string>"} with no code. A client that reads error.code has to tolerate error being a plain string.

Today's daily allowance is spent (application envelope)
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "استهلكت رصيد اليوم من طلبات API في خطة «المتطور» (1000 طلب يوميًا). يتجدد الرصيد عند منتصف الليل بتوقيت Asia/Riyadh. — Daily API quota exhausted: the المتطور plan allows 1000 calls per day. It resets at midnight Asia/Riyadh."
  }
}
Over 300 requests in a minute (Rack::Attack body, flat error)
{
  "error": "Rate limit exceeded. Please try again later."
}
Errors

Delete a file

DELETE/projects/{project_id}/files/{id}Requires files:destructive

Permanent. You may delete your OWN upload; deleting someone else's needs delete_any_file.

Path parameters

project_idstringrequired

The project REF, or its slug.

idstringrequired

The file's REF (fil-8f3kd), or its numeric id. Files never had a slug.

Request

cURL
curl -X DELETE "https://app.fareeqy.com/api/v1/projects/prj-8f3kd/files/108" \
  -H "Authorization: Bearer $FAREEQY_API_KEY"

Response

200
{
  "data": {
    "deleted": true,
    "file": {
      "id": 108,
      "name": "الهوية-البصرية.pdf"
    }
  }
}
Refusals:401403404429Show failure examples
401default

Missing or invalid API key.

default
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid or missing API key."
  }
}
403forbidden

Two different refusals share this status, and a client must tell them apart by error.code. forbidden means the key's scope or allowlist does not permit this operation, or Pundit denied the action. plan_upgrade_required means the company's plan carries no API access at all, so no key on it can ever succeed and there is nothing to retry.

This key may not perform this operation
{
  "error": {
    "code": "forbidden",
    "message": "This API key is not permitted to perform this operation."
  }
}
The company's plan carries no API access
{
  "error": {
    "code": "plan_upgrade_required",
    "message": "خطة «الاحترافي» لا تشمل الوصول إلى API. رقِّ إلى «المتطور» أو «الإنتاجي» لتفعيله. — The الاحترافي plan does not include API access. Upgrade to «المتطور» or «الإنتاجي» to enable it."
  }
}
404default

Resource not found or not accessible — also returned for EVERY endpoint when the company's rest_api feature flag is disabled (the surface is hidden). Lookups drill through the URL hierarchy, so another company's record is a 404 and never a leak. A path that matches no route at all answers 404 with the distinct code unknown_endpoint and echoes the path back, so a mistyped or half-built URL is told apart from a record that is missing or out of reach.

default
{
  "error": {
    "code": "not_found",
    "message": "Resource not found, or you do not have access to it."
  }
}
429rate_limit_exceeded

Either the company's daily API allowance is spent (rate_limit_exceeded), or the per-key / per-IP burst throttle of 300 requests per minute fired. Both come back after a wait, so Retry-After is honest here. The two bodies are not the same shape. The daily-quota refusal uses the standard error envelope. The burst throttle is served by Rack::Attack ahead of the application, so its body is a flat {"error": "<string>"} with no code. A client that reads error.code has to tolerate error being a plain string.

Today's daily allowance is spent (application envelope)
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "استهلكت رصيد اليوم من طلبات API في خطة «المتطور» (1000 طلب يوميًا). يتجدد الرصيد عند منتصف الليل بتوقيت Asia/Riyadh. — Daily API quota exhausted: the المتطور plan allows 1000 calls per day. It resets at midnight Asia/Riyadh."
  }
}
Over 300 requests in a minute (Rack::Attack body, flat error)
{
  "error": "Rate limit exceeded. Please try again later."
}
Errors

Delete a file by ref

DELETE/files/{id}Requires files:destructive

Permanent. There is no flat get or patch: a file's listing hangs off its project (and folder), and a file has no update on this surface, so deleting it is the only thing its own address is for.

Path parameters

idstringrequired

The file REF (fil-3qd8n). A numeric id is not accepted here.

Request

cURL
curl -X DELETE "https://app.fareeqy.com/api/v1/files/fil-3qd8n" \
  -H "Authorization: Bearer $FAREEQY_API_KEY"

Response

200
{
  "data": {
    "deleted": true,
    "file": {
      "id": 41,
      "name": "عقد-الموردين.pdf"
    }
  }
}
Refusals:401403404429Show failure examples
401default

Missing or invalid API key.

default
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid or missing API key."
  }
}
403forbidden

Two different refusals share this status, and a client must tell them apart by error.code. forbidden means the key's scope or allowlist does not permit this operation, or Pundit denied the action. plan_upgrade_required means the company's plan carries no API access at all, so no key on it can ever succeed and there is nothing to retry.

This key may not perform this operation
{
  "error": {
    "code": "forbidden",
    "message": "This API key is not permitted to perform this operation."
  }
}
The company's plan carries no API access
{
  "error": {
    "code": "plan_upgrade_required",
    "message": "خطة «الاحترافي» لا تشمل الوصول إلى API. رقِّ إلى «المتطور» أو «الإنتاجي» لتفعيله. — The الاحترافي plan does not include API access. Upgrade to «المتطور» or «الإنتاجي» to enable it."
  }
}
404default

Resource not found or not accessible — also returned for EVERY endpoint when the company's rest_api feature flag is disabled (the surface is hidden). Lookups drill through the URL hierarchy, so another company's record is a 404 and never a leak. A path that matches no route at all answers 404 with the distinct code unknown_endpoint and echoes the path back, so a mistyped or half-built URL is told apart from a record that is missing or out of reach.

default
{
  "error": {
    "code": "not_found",
    "message": "Resource not found, or you do not have access to it."
  }
}
429rate_limit_exceeded

Either the company's daily API allowance is spent (rate_limit_exceeded), or the per-key / per-IP burst throttle of 300 requests per minute fired. Both come back after a wait, so Retry-After is honest here. The two bodies are not the same shape. The daily-quota refusal uses the standard error envelope. The burst throttle is served by Rack::Attack ahead of the application, so its body is a flat {"error": "<string>"} with no code. A client that reads error.code has to tolerate error being a plain string.

Today's daily allowance is spent (application envelope)
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "استهلكت رصيد اليوم من طلبات API في خطة «المتطور» (1000 طلب يوميًا). يتجدد الرصيد عند منتصف الليل بتوقيت Asia/Riyadh. — Daily API quota exhausted: the المتطور plan allows 1000 calls per day. It resets at midnight Asia/Riyadh."
  }
}
Over 300 requests in a minute (Rack::Attack body, flat error)
{
  "error": "Rate limit exceeded. Please try again later."
}
Errors