API Documentation

API endpoint: https://2index.ninja/api/v1/

To make an API request, pass a Bearer access token in the Authorization header.

curl https://2index.ninja/api/v1/account -H "Authorization: Bearer API_TOKEN"

You can obtain the access token in the corresponding section of your account.

Every API response contains a success field that indicates whether the request succeeded. The errors field contains error messages.

In some cases, if a 403 error is returned, you may need to set a user-agent header, for example:

curl https://2index.ninja/api/v1/account -H "Authorization: Bearer API_TOKEN" -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"

Requests may also be throttled by your hosting provider — in that case we recommend using a proxy.

Account methods

Register a user

Send a POST request to register:

POST https://2index.ninja/api/v1/register

Request parameters:

Field Type Required Description
email string yes User email in [email protected] format.
source string no Registration source, defaults to api. May be, for example, wordpress_plugin.

Example responses:

Successful registration:

{
    "success": true,
    "message": "User registered successfully!",
    "account": {
        "email": "[email protected]",
        "tariff": "White Ninja",
        "available_projects": 1,
        "balance": 100.0,
        "available_links": 100,
        "available_indexation_check_links": 500,
        "link_sending_speed": 100,
        "available_link_sending_speed": 100,
        "tariff_available": true,
        "tariff_expiring_date": "2025-01-16T14:26:11.700740Z",
        "email_verified": false,
        "link_cost": "$0.00056",
        "api_key": "API_KEY"
    }
}

Possible errors:

Validation error:

{
    "errors": {
        "email": ["The email field is required."]
    }
}

Email already registered:

{
    "message": "The email has already been taken."
}

After a successful registration, an activation email will be sent to the specified address.

Get account data

Send a GET request to account:

GET https://2index.ninja/api/v1/account

The response is a JSON object:

{
    "success": true,
    "account": {
        "email": "[email protected]",
        "tariff": "White Ninja",
        "balance": 100.0,
        "available_projects": 1,
        "available_links": 100,
        "available_indexation_check_links": 500,
        "link_sending_speed": 100,
        "available_link_sending_speed": 45,
        "tariff_available": true,
        "tariff_expiring_date": "2025-01-16T14:26:11.000000Z",
        "email_verified": false,
        "link_cost": "$0.00056"
    }
}

Where:

Field Description
link_sending_speed Tariff link-sending speed limit (total per account per day).
available_link_sending_speed Remaining speed — link_sending_speed minus the sum of indexing_speed across all projects. That is, how much speed is still available to assign to new or existing projects without exceeding the limit.

Project methods

Get the list of projects

Send a GET request to project:

GET https://2index.ninja/api/v1/project

Pagination (optional)

By default (no parameters) all non-archived projects of the user are returned — this preserves backward compatibility.

If the page parameter is passed, the response switches to paginated mode and additionally contains a pagination block.

Parameter Type Required Description
page integer no Page number. If not passed — all projects are returned.
per_page integer no Page size. Accepted only together with page. Defaults to 20, allowed range 1…100 (values outside the range are clamped).

Example paginated request:

GET https://2index.ninja/api/v1/project?page=2&per_page=10

The response is a JSON object:

{
    "success": true,
    "projects": [
        {
            "id": 1,
            "name": "Project name",
            "type": "indexing",
            "website": "https://domain.com/",
            "status": "in progress",
            "created_at": "2024-03-01 14:34:56",
            "links_type": "internal",
            "google_account_access_granted": 0,
            "links_total": 500,
            "links_sending_speed": 400,
            "links_sent_google": 100,
            "links_sent_yandex": 40,
            "links_sent_bing": 100,
            "in_queue": 360,
            "sent_links": 100,
            "indexed": 30,
            "not_indexed": 0,
            "download_queue_url": "DOWNLOAD_QUEUE_URL",
            "download_sent_url": "DOWNLOAD_SENT_URL",
            "download_indexed_url": "DOWNLOAD_INDEXED_URL",
            "download_unindexed_url": "DOWNLOAD_UNINDEXED_URL"
        },
        {
            "id": "2",
            "name": "Project 2 name",
            "type": "indexing_check",
            "status": "in progress",
            "created_at": "2024-03-01 14:34:56",
            "links_total": 200,
            "links_checking_speed": 100,
            "in_queue": 100,
            "checked": 100,
            "indexed": 70,
            "not_indexed": 30,
            "download_queue_url": "DOWNLOAD_QUEUE_URL",
            "download_indexed_url": "DOWNLOAD_INDEXED_URL",
            "download_unindexed_url": "DOWNLOAD_UNINDEXED_URL",
            "download_all_url": "DOWNLOAD_ALL_URL",
            "download_checked_url": "DOWNLOAD_CHECKED_URL",
        }
        ...
    ]
}

In paginated mode (when page is passed) the response contains an additional pagination block:

{
    "success": true,
    "projects": [ ... ],
    "pagination": {
        "current_page": 2,
        "last_page": 5,
        "per_page": 10,
        "total": 47
    }
}

Get project data

Send a GET request to project/{project_id}:

GET https://2index.ninja/api/v1/project/1

The response is a JSON object:

  • For an indexing project:
{
    "success": true,
    "project": {
        "id": 1,
        "name": "Project name",
        "type": "indexing",
        "website": "https://domain.com/",
        "status": "in progress",
        "created_at": "2024-03-01 14:34:56",
        "links_type": "internal",
        "google_account_access_granted": 0,
        "links_total": 500,
        "links_sending_speed": 400,
        "links_sent_google": 100,
        "links_sent_yandex": 40,
        "links_sent_bing": 100,
        "in_queue": 360,
        "sent_links": 100,
        "indexed": 30,
        "not_indexed": 0,
        "download_queue_url": "DOWNLOAD_QUEUE_URL",
        "download_sent_url": "DOWNLOAD_SENT_URL",
        "download_indexed_url": "DOWNLOAD_INDEXED_URL",
        "download_unindexed_url": "DOWNLOAD_UNINDEXED_URL"
    }
}
  • For an indexing-check project:
{
    "success": true,
    "project": {
        "id": "2",
        "name": "Project 2 name",
        "type": "indexing_check",
        "status": "in progress",
        "created_at": "2024-03-01 14:34:56",
        "links_total": 200,
        "links_checking_speed": 100,
        "in_queue": 100,
        "checked": 100,
        "indexed": 70,
        "not_indexed": 30,
        "download_queue_url": "DOWNLOAD_QUEUE_URL",
        "download_indexed_url": "DOWNLOAD_INDEXED_URL",
        "download_unindexed_url": "DOWNLOAD_UNINDEXED_URL",
        "download_all_url": "DOWNLOAD_ALL_URL",
        "download_checked_url": "DOWNLOAD_CHECKED_URL"
    }
}

Create a project

Send a POST request to project:

POST https://2index.ninja/api/v1/project

Request parameters:

Field Type Required Description
name string yes Project name.
website string yes* Website URL in https://site.domain/ format.
*Required when project type is indexing.
for_external_links boolean no Project for external links. If not specified, will be for internal links only.
indexing_speed integer no** Project indexing speed. If not specified, the entire available account speed is used. Used for projects with type indexing. For projects with type indexing_check, checking_speed is used instead.
checking_speed integer no** Project link-checking speed. If not specified, the entire available account speed is used. Used for projects with type indexing_check. For projects with type indexing, indexing_speed is used instead.
type string no Project type.
Possible values: indexing — link indexing, indexing_check — link indexing check.
If not specified, an indexing link-indexing project is created.

** For any project type you can use either of the indexing_speed and checking_speed parameters — they are aliases of each other; if both are passed, indexing_speed is used.

The response is a JSON object:

{
    "success": true,
    "message": "The project has been successfully created"
}

Example of creating a link-indexing project: POST https://2index.ninja/api/v1/project

name: Project 1
website: https://website.com
for_external_links: 1
indexing_speed: 100

Example of creating an indexing-check project: POST https://2index.ninja/api/v1/project

name: Project 2
checking_speed: 100

Create (or get) a project for the WordPress plugin

Finds a project linked to a WordPress site. If no such project exists, creates a new one.

Send a POST request to account/get_wordpress_project:

POST https://2index.ninja/api/v1/account/get_wordpress_project

Request parameters:

Field Type Required Description
website string yes Website URL in https://site.domain/ format.

As a result of the request, either an existing user project matching the website URL is found, or a new project is created for that site. A new project will be configured for internal-link indexing with the maximum sending speed.

For an existing project the response is a JSON object:

{
    "success": true,
    "message": "Found existing project domain.com",
    "project": {
        "id": 1,
        "name": "Project name",
        "website": "https://domain.com/",
        "status": "in progress",
        "created_at": "2024-03-01 14:34:56",
        "links_type": "internal",
        "google_account_access_granted": 0,
        "links_total": 500,
        "links_sending_speed": 400,
        "links_sent_google": 100,
        "links_sent_yandex": 40,
        "links_sent_bing": 100,
        "in_queue": 360,
        "sent_links": 100,
        "indexed": 30,
        "not_indexed": 0,
        "download_queue_url": "DOWNLOAD_QUEUE_URL",
        "download_sent_url": "DOWNLOAD_SENT_URL",
        "download_indexed_url": "DOWNLOAD_INDEXED_URL",
        "download_unindexed_url": "DOWNLOAD_UNINDEXED_URL"
    }
}

For a newly created project the response is a JSON object:

{
    "success": true,
    "message": "The project has been successfully created",
    "project": {
        "id": 1,
        "name": "Project name",
        "website": "https://domain.com/",
        "status": "in progress",
        "created_at": "2024-03-01 14:34:56",
        "links_type": "internal",
        "google_account_access_granted": 0,
        "links_total": 500,
        "links_sending_speed": 400,
        "links_sent_google": 100,
        "links_sent_yandex": 40,
        "links_sent_bing": 100,
        "in_queue": 360,
        "sent_links": 100,
        "indexed": 30,
        "not_indexed": 0,
        "download_queue_url": "DOWNLOAD_QUEUE_URL",
        "download_sent_url": "DOWNLOAD_SENT_URL",
        "download_indexed_url": "DOWNLOAD_INDEXED_URL",
        "download_unindexed_url": "DOWNLOAD_UNINDEXED_URL"
    }
}

Clear the indexing queue

Send a POST request to project/{project_id}/clear_queue:

POST https://2index.ninja/api/v1/project/1/clear_queue

The response is a JSON object:

{
    "success": true,
    "message": "The indexing queue has been cleared successfully. X links have been removed"
}

Or the corresponding error message will be returned:

{
    "success": false,
    "errors": ["Project not found"]
}

Reset speed of completed projects

A bulk operation — sets indexing_speed = 0 for all completed projects of the user. Useful when, after bulk-completing projects, you want to free up the occupied speed without archiving them.

Send a POST request to project/reset_speed_completed:

POST https://2index.ninja/api/v1/project/reset_speed_completed

With no parameters it affects all the user's completed projects with indexing_speed > 0. All filter parameters are optional; dates are passed in Y-m-d format.

Field Type Required Description
created_at_from string no Project creation date, lower bound (Y-m-d).
created_at_to string no Project creation date, upper bound (Y-m-d).
completed_at_from string no Date of transition to completed, lower bound.
completed_at_to string no Date of transition to completed, upper bound.

The response is a JSON object:

{
    "success": true,
    "message": "Speed reset for 5 projects",
    "affected_projects": 5
}

For an invalid date format:

{
    "success": false,
    "errors": {
        "completed_at_from": ["The completed at from field must match the format Y-m-d."]
    }
}

Archive completed projects

A bulk operation — moves all completed projects of the user to archived, zeroing their speed. Useful for batch-closing completed projects without per-item requests.

Send a POST request to project/archive_completed:

POST https://2index.ninja/api/v1/project/archive_completed

Accepts the same optional date-range filters as reset_speed_completed:

Field Type Required Description
created_at_from string no Project creation date, lower bound (Y-m-d).
created_at_to string no Project creation date, upper bound (Y-m-d).
completed_at_from string no Date of transition to completed, lower bound.
completed_at_to string no Date of transition to completed, upper bound.

The response is a JSON object:

{
    "success": true,
    "message": "5 projects have been archived",
    "archived_count": 5
}

Link methods

Add links

Send a POST request to link/add:

POST https://2index.ninja/api/v1/link/add

Request parameters:

Field Required Description
project_id yes Project ID.
links yes List of links. Can be passed either as an array of links or as text (one link per line).
google no, if yandex or bing is specified Send links to Google.
yandex no, if google or bing is specified Send links to Yandex.
bing no, if google or yandex is specified Send links to Bing.
google_access_granted no Google account access granted.

The response is a JSON object:

{
    "success": true,
    "message": "Links have been successfully added to the project"
}

If the list contains invalid links, no links will be added and the corresponding error message will be returned:

{
    "success": false,
    "errors": ["You sent invalid links"],
    "invalid_links": ["http://wrong.link"]
}

For indexing-check projects, links are added exactly the same way as for indexing — google_access_granted is not required and will be ignored. Search engines must be specified, but currently only Google is supported for indexing checks.

Add links by project name

Send a POST request to link/add_simple. If no project with the specified name exists, it will be created automatically with the maximum sending speed.

POST https://2index.ninja/api/v1/link/add_simple

Request parameters:

Field Required Description
project_name no Project name. If not specified, a project named "default" will be created.
links yes List of links. Can be passed as an array or as text (one link per line).
google no, if yandex or bing is specified Send links to Google.
yandex no, if google or bing is specified Send links to Yandex.
bing no, if google or yandex is specified Send links to Bing.
google_access_granted no Google account access granted.

For indexing-check projects, links are added exactly the same way as for indexing — google_access_granted is not required and will be ignored. Search engines must be specified, but currently only Google is supported for indexing checks.

Request example:

{
    "project_name": "My Website",
    "links": ["https://example.com/page1", "https://example.com/page2"],
    "google": true,
    "yandex": false,
    "bing": true,
    "google_access_granted": false
}

Successful response:

{
    "success": true,
    "message": "Links have been successfully added to the project",
    "project_name": "My Website",
    "project_id": 12345
}

Possible errors:

  • If the list contains invalid links:
{
    "success": false,
    "errors": ["You sent invalid links"],
    "invalid_links": ["http://wrong.link"]
}
  • If project creation failed:
{
    "success": false,
    "errors": ["Failed to create the project"]
}

Check link status

Send a POST request to /api/v1/link/status:

POST https://2index.ninja/api/v1/link/status

Request parameters:

Field Required Description
project_id yes Project ID
link yes Link to check
  • On success the response is a JSON object:
{
    "success": true,
    "link": {
        "id": 34368960,
        "url": "https://2index.ninja/",
        "google": "Sent",
        "yandex": "Sent",
        "bing": "Sent",
        "is_external": 1,
        "google_sent": "2025-08-24 20:10:02",
        "yandex_sent": "2025-08-24 20:45:02",
        "bing_sent": "2025-08-24 20:05:05",
        "google_sent_attempts": 1,
        "yandex_sent_attempts": 1,
        "bing_sent_attempts": 1,
        "google_indexed": 1,
        "google_indexing_check_date": "2025-08-29"
    }
}

Where:

Field Description
id Link ID
url Link
google * Send status in Google
yandex * Send status in Yandex
bing * Send status in Bing
is_external External link
google_sent Date sent to Google
yandex_sent Date sent to Yandex
bing_sent Date sent to Bing
google_sent_attempts Number of send attempts to Google
yandex_sent_attempts Number of send attempts to Yandex
bing_sent_attempts Number of send attempts to Bing
google_indexed Indexed in Google
google_indexing_check_date Date of indexing check in Google

* Possible statuses:

Status Meaning
New New
Sent Sent
In the queue In the sending queue
In the process In the process of sending
Error sending An error occurred while sending
Don\'t index Do not index
Sent to reindexing Sent for reindexing
  • If an error occurs, a JSON object with the error message is returned. Examples:
{
    "success": false,
    "errors":{
        "link": [
            "The link field is required."
        ]
    }
}
{
    "success": false,
    "errors":[
        "Project not found"
    ]
}
{
    "success": false,
    "errors":[
        "URL not found"
    ]
}

You can also fetch the link status by link ID

Send a GET request to /api/v1/link/status/ID:

GET https://2index.ninja/api/v1/link/status/ID

The response is the same as in the previous variant.

Resend links for reindexing

Sends not-indexed links for reindexing. Accepts a max_attempts parameter that limits the maximum number of resend attempts (between 1 and 10). Accepts an array of links to resend for indexing; if no links are passed, all not-indexed links from the project are picked up. The method then checks the number of send attempts and tries to add the links to the project, taking account limits into account. If you pass links that were never sent before, they will also be added to the queue.

Send a POST request to /api/v1/link/resent_notindexed_links/{project_id}:

POST https://2index.ninja/api/v1/link/resent_notindexed_links/{project_id}

Request parameters:

Field Required Description
project_id yes Project ID
max_attempts yes (from 1 to 10) Indexing send attempts limit
links no — if omitted, ALL not-indexed links of the project will be used Links to resend
  • On success the response is a JSON object:
{
    "success": true,
    "message": "Links have been successfully added to the project. Invalid links was ignored.",
    "invalid_links": [],
    "project_id": 1,
    "ignored_links": [
        {
            "url": "https://domain.com/url1",
            "attempts": 2
        },
        {
            "url": "https://domain.com/url2",
            "attempts": 2
        }
    ]
}

Where:

Field Description
success Request success status
message Success message
invalid_links List of invalid links that were not added
project_id Project ID
ignored_links Ignored links — links that have already been sent max_attempts times or more

On error, a JSON object with error messages is returned. For example:

{
    "success": false,
    "errors": {
        "max_attempts": [
            "The max attempts field must not be greater than 10."
        ],
        "links": [
            "The links field must be an array."
        ]
    }
}

Or:

{
    "success": false,
    "errors": [
        "You do not have enough tokens to add any links. You need at least 1 tokens."
    ],
    "ignored_links": []
}

Delete links from a project

Deletes a link or an array of links from the project. If a link is still in the sending queue, the tokens spent on it are refunded to the balance. Already-sent links are deleted without a token refund.

Send a POST request to /api/v1/link/delete/{project_id}:

POST https://2index.ninja/api/v1/link/delete/{project_id}

Request parameters:

Field Required Description
project_id yes Project ID (passed in the URL).
link yes Link to delete. Can be a single string or an array of links.
  • On success the response is a JSON object:
{
    "success": true,
    "deleted": 3,
    "message": "The URLs have been deleted."
}

If the project contained several identical URLs (for example, the link was added multiple times), the response will additionally include a duplicates field:

{
    "success": true,
    "deleted": 5,
    "duplicates": 2,
    "message": "The URLs have been deleted, including duplicate (re-submitted) entries."
}

If none of the submitted links were found in the project:

{
    "success": true,
    "deleted": 0,
    "message": "No URLs were found to delete."
}

Possible errors:

{
    "success": false,
    "errors": ["You cannot work with links. The project is deeply archived"]
}
{
    "success": false,
    "errors": "Project not found"
}

Working with link sources

Link sources are available for any project type and are processed identically. For indexing-check projects only Google can be selected.

Adding a sitemap

Send a POST request to /api/v1/sitemap/add:

POST https://2index.ninja/api/v1/sitemap/add

Request parameters:

Field Required Description
project_id yes Project ID
sitemap yes Sitemap URL
google no, if yandex or bing is specified Send links to Google
yandex no, if google or bing is specified Send links to Yandex
bing no, if google or yandex is specified Send links to Bing
google_access_granted no Google account access granted
watch no Watch for sitemap changes

Possible errors:

Error
Data validation error. One of the required fields is missing or has an invalid value.
Tariff unavailable. The user's tariff plan has expired.
Project not found. The user does not own the specified project or the project does not exist.
Internal server error.

Example responses:

Sitemap added successfully:

{
    "success": true,
    "message": "The sitemap has been successfully added; we will download it as soon as possible and send a notification by email once finished."
}

Validation error:

{
    "errors": ["sitemap":["The sitemap field is required."]]
}

Project not found:

{
    "errors": ["Project not found"]
}

Updating sitemap watch status

Send a POST request to /api/v1/sitemap/update_watch:

POST https://2index.ninja/api/v1/sitemap/update_watch

Request parameters:

Field Type Required Description
project_id integer yes Project ID
sitemap_id integer yes Sitemap ID
watch boolean yes Whether to watch for sitemap changes

Possible errors:

Error
Data validation error. One of the required fields is missing or has an invalid value.
Project or link source not found. The user does not own the specified project or the project/source does not exist.
Internal server error.

Example responses:

Watch status updated successfully:

{
    "success": true
}

Link source not found:

{
    "errors": ["Link source not found"]
}

Deleting a link source

Send a POST request to /api/v1/sitemap/delete:

POST https://2index.ninja/api/v1/sitemap/delete

Request parameters:

Field Type Required Description
project_id integer yes Project ID
sitemap_id integer yes Sitemap ID

Possible errors:

Error
Data validation error. One of the required fields is missing or has an invalid value.
Project or link source not found. The user does not own the specified project or the project/source does not exist.
Internal server error.

Example responses:

Link source deleted successfully:

{
    "success": true,
    "message": "The link source has been successfully deleted"
}

Project not found:

{
    "errors": ["Project not found"]
}

List of link sources

To get the list of added link sources, send a POST request to https://2index.ninja/api/v1/link_sources.

Pass project_id in the request.

The result is a data set like this:

[
    {
        "id": 1247,
        "project_id": 3540,
        "name": "urls.txt",
        "type": "text file",
        "created_at": "2025-01-17T15:51:43.000000Z",
        "processing_date": "2025-01-17T15:51:48.000000Z",
        "has_error": false,
        "error_message": "",
        "status": "success",
        "is_pending": false,
        "is_success": true,
        "total_links": 29585,
        "added_links": 29582,
        "invalid_links": 3,
        "watch": 0,
        "google_access_granted": 0,
        "is_external_links": 1,
        "search_engines": {
            "google": "1"
        }
    },
    {
        "id": 1246,
        "project_id": 3540,
        "name": "urls.txt",
        "type": "text file",
        "created_at": "2025-01-17T15:50:59.000000Z",
        "processing_date": "2025-01-17T15:51:02.000000Z",
        "has_error": false,
        "error_message": "",
        "status": "success",
        "is_pending": false,
        "is_success": true,
        "total_links": 29585,
        "added_links": 10440,
        "invalid_links": 1,
        "watch": 0,
        "google_access_granted": 0,
        "is_external_links": 1,
        "search_engines": {
            "google": "1"
        }
    },
    {
        "id": 1245,
        "project_id": 3540,
        "name": "urls.txt",
        "type": "text file",
        "created_at": "2025-01-17T15:46:17.000000Z",
        "processing_date": null,
        "has_error": false,
        "error_message": null,
        "status": "pending",
        "is_pending": true,
        "is_success": false,
        "total_links": 0,
        "added_links": 0,
        "invalid_links": 0,
        "watch": 0,
        "google_access_granted": 0,
        "is_external_links": 1,
        "search_engines": {
            "google": "1"
        }
    },
    {
        "id": 1244,
        "project_id": 3540,
        "name": "urls.txt",
        "type": "text file",
        "created_at": "2025-01-17T14:52:07.000000Z",
        "processing_date": "2025-01-17T14:53:06.000000Z",
        "has_error": true,
        "error_message": "Unable to encode attribute [invalid_links_list] for model [App\\Models\\LinkSource\\LinkSource] to JSON: Malformed UTF-8 characters, possibly incorrectly encoded.",
        "status": "error",
        "is_pending": false,
        "is_success": false,
        "total_links": 29585,
        "added_links": 0,
        "invalid_links": 3,
        "watch": 0,
        "google_access_granted": 0,
        "is_external_links": 1,
        "search_engines": {
            "google": "1"
        }
    }
]

Where

id — link source ID project_id — project ID name — name of the attached file or sitemap URL type — link source type (text file or sitemap) — can be: sitemap, text file created_at — creation date processing_date — processing date has_error — whether a processing error occurred error_message — error message status — status — can be: pending (waiting to be processed), error (processing error), success is_pending — currently being processed is_success — successful processing total_links — total links found added_links — links added invalid_links — invalid links watch — sitemap watch status google_access_granted — whether Google account access has been granted is_external_links — external links search_engines — connected search engines

PHP implementation example


<?php

/**
 * A class for working with the 2Index.Ninja service API.
 */
class API_2IndexNinja
{
    /**
     * API endpoint URL
     * @var string
     */
    private $url = 'https://2index.ninja/api/v1/';

    /**
     * API access token
     * @var string
     */
    private $access_token;

    /**
     * Class constructor.
     *
     * @param string $access_token API authorization access token.
     */
    public function __construct($access_token)
    {
        $this->access_token = $access_token;
    }

    /*
    |--------------------------------------------------------------------------
    | Account methods
    |--------------------------------------------------------------------------
    */

    /**
     * Gets information about the current account.
     *
     * @return array|null Decoded API response with account data.
     */
    public function account()
    {
        return $this->make_request('account');
    }

    /**
     * Registers a new user.
     *
     * @param string $email New user email.
     * @param string $source Registration source.
     * @return array|null Decoded API response.
     */
    public function register_user($email, $source)
    {
        return $this->make_request('register', [
            'email'  => $email,
            'source' => $source,
        ]);
    }

    /*
    |--------------------------------------------------------------------------
    | Project methods
    |--------------------------------------------------------------------------
    */

    /**
     * Gets the list of the user's projects.
     *
     * Without parameters returns all non-archived projects. If $page is passed,
     * pagination is enabled and the response includes a 'pagination' block.
     *
     * @param int|null $page     Page number (when set — pagination is enabled).
     * @param int|null $per_page Page size (1..100, defaults to 20). Only used together with $page.
     * @return array|null Decoded API response with the list of projects.
     */
    public function projects_list($page = null, $per_page = null)
    {
        $url_data = [];
        if ($page) {
            $url_data['page'] = $page;

            if ($per_page) {
                $url_data['per_page'] = $per_page;
            }
        }

        $url_data = $url_data ? ('?' . http_build_query($url_data)) : '';

        return $this->make_request('project' . $url_data);
    }

    /**
     * Gets data of a specific project by its ID.
     *
     * @param int $id Project ID.
     * @return array|null Decoded API response with project data.
     */
    public function project($id)
    {
        return $this->make_request('project/' . $id);
    }

    /**
     * Adds a new project for link indexing.
     *
     * @param string $name Project name.
     * @param string $website Project website.
     * @param bool $for_external_links Whether it is for external links (0 or 1).
     * @param int|null $indexing_speed Indexing speed.
     * @return array|null Decoded API response with the created project data.
     */
    public function add_project($name, $website, $for_external_links = 0, $indexing_speed = null)
    {
        return $this->make_request('project', [
            'name'               => $name,
            'website'            => $website,
            'for_external_links' => $for_external_links,
            'indexing_speed'     => $indexing_speed,
            'type'               => 'indexing',
        ]);
    }

    /**
     * Adds a new project for indexing check.
     *
     * @param string $name Project name.
     * @param int|null $checking_speed Checking speed.
     * @return array|null Decoded API response with the created project data.
     */
    public function add_indexing_check_project($name, $checking_speed = null)
    {
        return $this->make_request('project', [
            'name'           => $name,
            'checking_speed' => $checking_speed,
            'type'           => 'indexing_check',
        ]);
    }

    /**
     * Finds a project linked to a WordPress site.
     * If no such project exists, creates a new one.
     *
     * @param string $website Website URL to look up the project by.
     * @return array|null Decoded API response with project data.
     */
    public function get_wordpress_project($website)
    {
        return $this->make_request('account/get_wordpress_project', ['website' => $website]);
    }


    /**
     * Clears the link queue for a project.
     *
     * @param int $project_id Project ID.
     * @return array|null Decoded API response.
     */
    public function clear_queue($project_id)
    {
        return $this->make_request('project/' . $project_id . '/clear_queue', true);
    }

    /**
     * Resets the indexing speed of all the user's completed projects.
     * All filter parameters are optional; dates are in Y-m-d format.
     *
     * @param string|null $created_at_from
     * @param string|null $created_at_to
     * @param string|null $completed_at_from
     * @param string|null $completed_at_to
     * @return array|null Decoded response: success, message, affected_projects.
     */
    public function reset_speed_completed(
        $created_at_from = null,
        $created_at_to = null,
        $completed_at_from = null,
        $completed_at_to = null
    ) {
        $payload = array_filter([
            'created_at_from'   => $created_at_from,
            'created_at_to'     => $created_at_to,
            'completed_at_from' => $completed_at_from,
            'completed_at_to'   => $completed_at_to,
        ]);

        // Without parameters: an empty array is treated by make_request() as
        // "no POST data" and a GET is sent instead, which gets matched by the
        // project/{id} route. So when filters are empty we force POST via true.
        return $this->make_request('project/reset_speed_completed', $payload ?: true);
    }

    /**
     * Archives all the user's completed projects.
     * Accepts the same optional date-range filters as reset_speed_completed.
     *
     * @param string|null $created_at_from
     * @param string|null $created_at_to
     * @param string|null $completed_at_from
     * @param string|null $completed_at_to
     * @return array|null Decoded response: success, message, archived_count.
     */
    public function archive_completed(
        $created_at_from = null,
        $created_at_to = null,
        $completed_at_from = null,
        $completed_at_to = null
    ) {
        $payload = array_filter([
            'created_at_from'   => $created_at_from,
            'created_at_to'     => $created_at_to,
            'completed_at_from' => $completed_at_from,
            'completed_at_to'   => $completed_at_to,
        ]);

        return $this->make_request('project/archive_completed', $payload ?: true);
    }

    /*
    |--------------------------------------------------------------------------
    | Link methods
    |--------------------------------------------------------------------------
    */

    /**
     * Adds links to the specified project.
     *
     * @param int $project_id Project ID.
     * @param array|string $links Array of links or a single link as a string.
     * @param bool $google Whether to send to Google.
     * @param bool $yandex Whether to send to Yandex.
     * @param bool $bing Whether to send to Bing.
     * @param bool $google_access_granted Flag confirming Google access.
     * @return array|null Decoded API response.
     */
    public function add_links($project_id, $links, $google, $yandex, $bing, $google_access_granted = false)
    {
        return $this->make_request('link/add', [
            'project_id'            => $project_id,
            'links'                 => $links,
            'google'                => $google,
            'yandex'                => $yandex,
            'bing'                  => $bing,
            'google_access_granted' => $google_access_granted,
        ]);
    }

    /**
     * Adds links to a project by name. If the project doesn't exist, it will be created.
     *
     * @param array|string $links Array of links or a single link as a string.
     * @param bool $google Whether to send to Google.
     * @param bool $yandex Whether to send to Yandex.
     * @param bool $bing Whether to send to Bing.
     * @param string $project_name Project name (defaults to 'default').
     * @param bool $google_access_granted Flag confirming Google access.
     * @return array|null Decoded API response.
     */
    public function add_links_simple($links, $google, $yandex, $bing, $project_name = 'default', $google_access_granted = false)
    {
        return $this->make_request('link/add_simple', [
            'project_name'          => $project_name,
            'links'                 => $links,
            'google'                => $google,
            'yandex'                => $yandex,
            'bing'                  => $bing,
            'google_access_granted' => $google_access_granted,
        ]);
    }

    /**
     * Gets the link sources for a project.
     *
     * @param int $project_id Project ID.
     * @return array|null Decoded API response.
     */
    public function link_sources($project_id)
    {
        return $this->make_request('link_sources', [
            'project_id' => $project_id
        ]);
    }

    /**
     * Gets the status of the specified link.
     * If the same link was sent several times, returns the status of the latest one.
     *
     * @param int $project_id Project ID
     * @param string $link Link to check
     * @return array Link status
     */
    public function checkLink($project_id, $link)
    {
        return $this->make_request('link/status', [
            'project_id' => $project_id,
            'link' => $link,
        ]);
    }

    /**
     * Gets the status of the specified link.
     *
     * @param int $link_id Link ID
     * @return array Link status
     */
    public function checkLinkById($link_id)
    {
        return $this->make_request('link/status/' . $link_id);
    }

    /**
     * Resends a project's not-indexed links for indexing.
     * If $links is not passed, all not-indexed links of the project are used,
     * those whose attempt count is less than $max_attempts.
     *
     * @param int $project_id Project ID.
     * @param int $max_attempts Maximum send attempts (1..10).
     * @param array|null $links Specific list of links to resend.
     * @return array|null
     */
    public function resent_notindexed_links($project_id, $max_attempts, $links = null)
    {
        return $this->make_request('link/resent_notindexed_links/' . $project_id, [
            'max_attempts' => $max_attempts,
            'links'        => $links,
        ]);
    }

    /**
     * Deletes a link or an array of links from a project.
     *
     * @param int $project_id Project ID.
     * @param string|array $link Link to delete (or an array).
     * @return array|null
     */
    public function deleteUrls($project_id, $link)
    {
        return $this->make_request('link/delete/' . $project_id, [
            'link' => $link,
        ]);
    }

    /*
    |--------------------------------------------------------------------------
    | Link source methods
    |--------------------------------------------------------------------------
    */

    /**
     * Adds a Sitemap to a project.
     *
     * @param int $project_id Project ID.
     * @param string $sitemap_url Sitemap file URL.
     * @param bool $google Whether to send to Google.
     * @param bool $yandex Whether to send to Yandex.
     * @param bool $bing Whether to send to Bing.
     * @param bool $google_access_granted Flag confirming Google access.
     * @param bool $watch Whether to enable watching for Sitemap changes.
     * @return array|null Decoded API response.
     */
    public function add_sitemap($project_id, $sitemap_url, $google, $yandex, $bing, $google_access_granted = false, $watch = false)
    {
        return $this->make_request('sitemap/add', [
            'project_id'            => $project_id,
            'sitemap'               => $sitemap_url,
            'google'                => $google,
            'yandex'                => $yandex,
            'bing'                  => $bing,
            'google_access_granted' => $google_access_granted,
            'watch'                 => $watch,
        ]);
    }

    /**
     * Enables or disables watching for changes for a Sitemap.
     *
     * @param int $project_id Project ID.
     * @param int $sitemap_id Sitemap file ID.
     * @param bool $watch New watch status (true — enable, false — disable).
     * @return array|null Decoded API response.
     */
    public function update_sitemap_watch($project_id, $sitemap_id, $watch)
    {
        return $this->make_request('sitemap/update_watch', [
            'project_id' => $project_id,
            'sitemap_id' => $sitemap_id,
            'watch'      => $watch,
        ]);
    }

    /**
     * Removes a Sitemap from a project.
     *
     * @param int $project_id Project ID.
     * @param int $sitemap_id Sitemap file ID to remove.
     * @return array|null Decoded API response.
     */
    public function sitemap_delete($project_id, $sitemap_id)
    {
        return $this->make_request('sitemap/delete', [
            'project_id' => $project_id,
            'sitemap_id' => $sitemap_id,
        ]);
    }

    /**
     * Performs an API request.
     *
     * @param string $endpoint API endpoint to call.
     * @param array|true $post_data POST data. If true, an empty POST is sent.
     * @return array|null Decoded JSON API response or null on failure.
     */
    private function make_request($endpoint, $post_data = [])
    {
        $ch = curl_init($this->url . $endpoint);

        if ($post_data === true) {
            curl_setopt($ch, CURLOPT_POST, 1);
        } elseif ($post_data) {
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
        }

        curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer " . $this->access_token]);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, false);

        // Sets the user-agent — required if you start getting 403 errors.
        // curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36');

        $result = curl_exec($ch);
        curl_close($ch);

        return json_decode($result, 1);
    }
}