Projects

Projects the caller can access.

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

The resource

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

idstring

The project slug (stable public id).

namestring
descriptionstring | null
is_publicboolean
archivedboolean
start_datestring (date) | null
end_datestring (date) | null
task_lists_countinteger

Number of task lists (counter cache).

tasks_countinteger

Number of tasks across the project's lists.

created_atstring (date-time)
updated_atstring (date-time)
Project
{
  "id": "تطوير-الموقع",
  "name": "تطوير الموقع",
  "description": "إعادة بناء الموقع التعريفي وربطه بالمنتج.",
  "is_public": false,
  "archived": false,
  "start_date": "2026-07-01",
  "end_date": "2026-09-30",
  "task_lists_count": 4,
  "tasks_count": 27,
  "created_at": "2026-07-01T09:15:00.000+03:00",
  "updated_at": "2026-07-14T12:22:31.000+03:00"
}

List accessible projects

GET/projectsRequires projects:read

Projects the caller can access (owned / public / direct-member / team-member), newest first. Excludes archived unless include_archived.

Query parameters

include_archivedboolean

Include archived projects.

Default: false

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" \
  -H "Authorization: Bearer $FAREEQY_API_KEY"

Response

200
{
  "data": [
    {
      "id": "تطوير-الموقع",
      "name": "تطوير الموقع",
      "description": "إعادة بناء الموقع التعريفي وربطه بالمنتج.",
      "is_public": false,
      "archived": false,
      "start_date": "2026-07-01",
      "end_date": "2026-09-30",
      "task_lists_count": 4,
      "tasks_count": 27,
      "created_at": "2026-07-01T09:15:00.000+03:00",
      "updated_at": "2026-07-14T12:22:31.000+03:00"
    }
  ],
  "meta": {
    "total": 4,
    "limit": 50,
    "offset": 0,
    "count": 4
  }
}
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

Create a project

POST/projectsRequires projects:write

Request body

namestringrequired
descriptionstring | null
start_datestring | null

ISO 8601 date.

end_datestring | null

ISO 8601 date.

is_publicboolean

Request

cURL
curl -X POST "https://app.fareeqy.com/api/v1/projects" \
  -H "Authorization: Bearer $FAREEQY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"تطوير الموقع","description":"إعادة بناء الموقع التعريفي وربطه بالمنتج.","start_date":"2026-07-01","end_date":"2026-09-30","is_public":false}'

Response

201
{
  "data": {
    "id": "تطوير-الموقع",
    "name": "تطوير الموقع",
    "description": "إعادة بناء الموقع التعريفي وربطه بالمنتج.",
    "is_public": false,
    "archived": false,
    "start_date": "2026-07-01",
    "end_date": "2026-09-30",
    "task_lists_count": 4,
    "tasks_count": 27,
    "created_at": "2026-07-01T09:15:00.000+03:00",
    "updated_at": "2026-07-14T12:22:31.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

Get a project

GET/projects/{id}Requires projects:read

Path parameters

idstringrequired

The project SLUG (unique within the company).

Request

cURL
curl "https://app.fareeqy.com/api/v1/projects/تطوير-الموقع" \
  -H "Authorization: Bearer $FAREEQY_API_KEY"

Response

200
{
  "data": {
    "id": "تطوير-الموقع",
    "name": "تطوير الموقع",
    "description": "إعادة بناء الموقع التعريفي وربطه بالمنتج.",
    "is_public": false,
    "archived": false,
    "start_date": "2026-07-01",
    "end_date": "2026-09-30",
    "task_lists_count": 4,
    "tasks_count": 27,
    "created_at": "2026-07-01T09:15:00.000+03:00",
    "updated_at": "2026-07-14T12:22:31.000+03:00"
  }
}
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

Update a project

PATCH/projects/{id}Requires projects:write

Path parameters

idstringrequired

The project SLUG (unique within the company).

Request body

namestring
descriptionstring | null
start_datestring | null
end_datestring | null
is_publicboolean

Request

cURL
curl -X PATCH "https://app.fareeqy.com/api/v1/projects/تطوير-الموقع" \
  -H "Authorization: Bearer $FAREEQY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"description":"إعادة بناء الموقع التعريفي وربطه بالمنتج وبلوحة القياس.","end_date":"2026-10-15"}'

Response

200
{
  "data": {
    "id": "تطوير-الموقع",
    "name": "تطوير الموقع",
    "description": "إعادة بناء الموقع التعريفي وربطه بالمنتج.",
    "is_public": false,
    "archived": false,
    "start_date": "2026-07-01",
    "end_date": "2026-09-30",
    "task_lists_count": 4,
    "tasks_count": 27,
    "created_at": "2026-07-01T09:15:00.000+03:00",
    "updated_at": "2026-07-14T12:22:31.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 project

DELETE/projects/{id}Requires projects:destructive

Permanent, cascading delete (demands manage_projects). Prefer archive. The response reports the counts destroyed.

Path parameters

idstringrequired

The project SLUG (unique within the company).

Request

cURL
curl -X DELETE "https://app.fareeqy.com/api/v1/projects/تطوير-الموقع" \
  -H "Authorization: Bearer $FAREEQY_API_KEY"

Response

200
{
  "data": {
    "deleted": true,
    "project": {
      "name": "تطوير الموقع",
      "slug": "تطوير-الموقع"
    },
    "destroyed": {
      "task_lists": 4,
      "tasks": 27,
      "discussions": 3,
      "files": 12,
      "folders": 2
    }
  }
}
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

Archive a project

POST/projects/{id}/archiveRequires projects:write

Reversible put-away; records a timeline event and freezes writes across the project. Prefer this over delete.

Path parameters

idstringrequired

The project SLUG (unique within the company).

Request

cURL
curl -X POST "https://app.fareeqy.com/api/v1/projects/تطوير-الموقع/archive" \
  -H "Authorization: Bearer $FAREEQY_API_KEY"

Response

200
{
  "data": {
    "id": "تطوير-الموقع",
    "name": "تطوير الموقع",
    "description": "إعادة بناء الموقع التعريفي وربطه بالمنتج.",
    "is_public": false,
    "archived": false,
    "start_date": "2026-07-01",
    "end_date": "2026-09-30",
    "task_lists_count": 4,
    "tasks_count": 27,
    "created_at": "2026-07-01T09:15:00.000+03:00",
    "updated_at": "2026-07-14T12:22:31.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

Unarchive a project

POST/projects/{id}/unarchiveRequires projects:write

Path parameters

idstringrequired

The project SLUG (unique within the company).

Request

cURL
curl -X POST "https://app.fareeqy.com/api/v1/projects/تطوير-الموقع/unarchive" \
  -H "Authorization: Bearer $FAREEQY_API_KEY"

Response

200
{
  "data": {
    "id": "تطوير-الموقع",
    "name": "تطوير الموقع",
    "description": "إعادة بناء الموقع التعريفي وربطه بالمنتج.",
    "is_public": false,
    "archived": false,
    "start_date": "2026-07-01",
    "end_date": "2026-09-30",
    "task_lists_count": 4,
    "tasks_count": 27,
    "created_at": "2026-07-01T09:15:00.000+03:00",
    "updated_at": "2026-07-14T12:22:31.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