# RhinoTenders API

> REST API reference for RhinoTenders subscribers: status, tenders list, filters, organizations and tender detail.

## Getting started

- Base URL: `https://api.rhinotenders.com`
- It is a read-only REST API (all endpoints are GET). Access must be enabled by a RhinoTenders admin for your account, then you generate your own token from your https://rhinotenders.com/dashboard/api.
- All endpoints are `GET` and read-only.
- Rate limit: 30 requests/minute per account, shared across all endpoints below.

The RhinoTenders Data API lets your own integrations pull the tenders catalogue you already have access to through your subscription: status check, list, filters, organizations and tender details.

## Authentication

Every request must include your token as a Bearer token in the Authorization header:

```
Authorization: Bearer <your_token>
```

Your token is shown only once when generated (or regenerated) from the dashboard — store it securely, it cannot be retrieved again.

## Status

`GET /api/v1/data/status`

Checks that your token is valid and returns your api access, subscription and quota status in a single call. Useful to verify your integration. Free to call, does not consume any quota.

Request (cURL):

```curl
curl -H "Authorization: Bearer <your_token>" \
  "https://api.rhinotenders.com/api/v1/data/status"
```

Request (PHP):

```php
$ch = curl_init('https://api.rhinotenders.com/api/v1/data/status');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer <your_token>',
]);

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

$data = json_decode($response, true);
```

Request (Python):

```python
import requests

response = requests.get(
    "https://api.rhinotenders.com/api/v1/data/status",
    headers={"Authorization": "Bearer <your_token>"},
)

data = response.json()
```

Request (JavaScript):

```js
const response = await fetch("https://api.rhinotenders.com/api/v1/data/status", {
  headers: { Authorization: "Bearer <your_token>" },
})

const data = await response.json()
```

Response:

```json
{
  "success": true,
  "data": {
    "token": { "name": "api-client", "created_at": "2026-08-01T10:00:00Z", "expires_at": "2027-08-01T10:00:00Z" },
    "api_access": { "active": true },
    "subscription": { "plan": "Standard", "status": "active" },
    "quota": {
      "daily": { "limit": 100, "used": 7, "remaining": 93, "resets_at": "2026-09-24T00:00:00Z" },
      "total": { "limit": null, "used": null, "remaining": null }
    }
  }
}
```

## List tenders

`GET /api/v1/data/tenders`

Returns the tenders matching the given filters. Same parameters as the site search (text search, activity, location, status, type, organization, dates). Advanced filters (organization, end date) are silently ignored if your plan doesn't grant access to them — never an error. Free to call, consumes no quota, but blocked (403) if your subscription isn't active or your view quota is already exhausted.

| Parameter | Description |
| --- | --- |
| `tender_id` | A single tender ID — bypasses all other filters. |
| `tender_ids` | Multiple comma-separated IDs — bypasses all other filters. |
| `search` | Text search (title, code). |
| `activity_id` | Activity ID(s), comma-separated. |
| `location_id` | Location ID(s), comma-separated. |
| `status_id` | Status ID(s), comma-separated. |
| `organism_id` | Organization ID(s) — advanced filter, ignored if not granted by your plan. |
| `tender_type` | National, International, or both comma-separated. |
| `publish_date_start / publish_date_end` | Publish date range (YYYY-MM-DD). |
| `ending_date_start / ending_date_end` | End date range — advanced filter, ignored if not granted by your plan. |
| `page / per_page` | Pagination — per_page defaults to 20, capped at 100. |

Request (cURL):

```curl
curl -H "Authorization: Bearer <your_token>" \
  "https://api.rhinotenders.com/api/v1/data/tenders?page=1&per_page=20&activity_id=12000000"
```

Request (PHP):

```php
$ch = curl_init('https://api.rhinotenders.com/api/v1/data/tenders?page=1&per_page=20&activity_id=12000000');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer <your_token>',
]);

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

$data = json_decode($response, true);
```

Request (Python):

```python
import requests

response = requests.get(
    "https://api.rhinotenders.com/api/v1/data/tenders?page=1&per_page=20&activity_id=12000000",
    headers={"Authorization": "Bearer <your_token>"},
)

data = response.json()
```

Request (JavaScript):

```js
const response = await fetch("https://api.rhinotenders.com/api/v1/data/tenders?page=1&per_page=20&activity_id=12000000", {
  headers: { Authorization: "Bearer <your_token>" },
})

const data = await response.json()
```

Response:

```json
{
  "success": true,
  "data": [
    {
      "id": 1490301,
      "title": "Etude et suivi realisation de 4 classes en extention",
      "organism": { "id": 3358, "name": "APC - Commune d'El Milia Wilaya de Jijel" },
      "location": { "id": 20, "name": "Jijel" },
      "status": { "id": 4, "name": "Résultats" },
      "publish_date": "2026-09-09",
      "ending_date": "2026-09-20T08:00:00Z"
    }
  ],
  "meta": { "current_page": 1, "per_page": 20, "total": 187 }
}
```

## Filter data

`GET /api/v1/data/tenders/filters`

Reference data to build your filters: activities, statuses, locations, types. A single complete object (no pagination), server-cached for 1-2h. hasAdvancedFilters reflects your subscription plan.

Request (cURL):

```curl
curl -H "Authorization: Bearer <your_token>" \
  "https://api.rhinotenders.com/api/v1/data/tenders/filters"
```

Request (PHP):

```php
$ch = curl_init('https://api.rhinotenders.com/api/v1/data/tenders/filters');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer <your_token>',
]);

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

$data = json_decode($response, true);
```

Request (Python):

```python
import requests

response = requests.get(
    "https://api.rhinotenders.com/api/v1/data/tenders/filters",
    headers={"Authorization": "Bearer <your_token>"},
)

data = response.json()
```

Request (JavaScript):

```js
const response = await fetch("https://api.rhinotenders.com/api/v1/data/tenders/filters", {
  headers: { Authorization: "Bearer <your_token>" },
})

const data = await response.json()
```

Response:

```json
{
  "success": true,
  "data": {
    "locations": [{ "id": 20, "name": "Jijel", "type": "wilaya", "flag": null, "country_name": "Algérie" }],
    "statuses": [{ "id": 4, "name": "Résultats" }],
    "activities": [{ "id": 7233, "name": "[72110000] - Travaux de bâtiment" }],
    "types": [
      { "value": "National", "label": "National" },
      { "value": "International", "label": "International" }
    ],
    "hasAdvancedFilters": true
  }
}
```

## Organizations

`GET /api/v1/data/organizations`

Search organizations by name (q, accent/case-insensitive) to resolve an organism_id for the tenders list filter, or paginate through all of them to sync the directory. Only organizations that have published at least one tender — never the full internal directory.

| Parameter | Description |
| --- | --- |
| `q` | Search by name, accent/case-insensitive. |
| `page / per_page` | Pagination — per_page defaults to 50, capped at 500. |

Request (cURL):

```curl
curl -H "Authorization: Bearer <your_token>" \
  "https://api.rhinotenders.com/api/v1/data/organizations?q=education&per_page=50"
```

Request (PHP):

```php
$ch = curl_init('https://api.rhinotenders.com/api/v1/data/organizations?q=education&per_page=50');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer <your_token>',
]);

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

$data = json_decode($response, true);
```

Request (Python):

```python
import requests

response = requests.get(
    "https://api.rhinotenders.com/api/v1/data/organizations?q=education&per_page=50",
    headers={"Authorization": "Bearer <your_token>"},
)

data = response.json()
```

Request (JavaScript):

```js
const response = await fetch("https://api.rhinotenders.com/api/v1/data/organizations?q=education&per_page=50", {
  headers: { Authorization: "Bearer <your_token>" },
})

const data = await response.json()
```

Response:

```json
{
  "success": true,
  "data": [
    { "id": 42, "name": "Ministère de l'Éducation" }
  ],
  "meta": { "current_page": 1, "per_page": 50, "total": 3 }
}
```

## Tender detail

`GET /api/v1/data/tenders/{id}`

Full tender detail. Consumes one unit of your subscription quota — the exact same quota as browsing the tender on the website (a tender viewed once, via the API or the site, is not counted twice within 12 months). Exception: "Online consultation" tenders and tenders expired for more than 10 days are freely accessible, with no consumption. The response shape depends on the tender's status (see the two examples below).

Request (cURL):

```curl
curl -H "Authorization: Bearer <your_token>" \
  "https://api.rhinotenders.com/api/v1/data/tenders/1490301"
```

Request (PHP):

```php
$ch = curl_init('https://api.rhinotenders.com/api/v1/data/tenders/1490301');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer <your_token>',
]);

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

$data = json_decode($response, true);
```

Request (Python):

```python
import requests

response = requests.get(
    "https://api.rhinotenders.com/api/v1/data/tenders/1490301",
    headers={"Authorization": "Bearer <your_token>"},
)

data = response.json()
```

Request (JavaScript):

```js
const response = await fetch("https://api.rhinotenders.com/api/v1/data/tenders/1490301", {
  headers: { Authorization: "Bearer <your_token>" },
})

const data = await response.json()
```

Response — Standard tender:

```json
{
  "success": true,
  "data": {
    "id": 1490301,
    "slug": "etude-et-suivi-realisation-de-4-classes-en-extention",
    "title": "Etude et suivi realisation de 4 classes en extention",
    "code": "07/2026",
    "publish_date": "2026-09-09",
    "ending_date": "2026-09-20T08:00:00Z",
    "is_expired": true,
    "status": { "id": 4, "name": "Résultats" },
    "tender_type": "National",
    "qualification": null,
    "address_cdc": "Étude de Maître Belhadj Ali, huissier de justice, Boumerdès",
    "cdc_price": { "amount": "16000000.00", "currency": "DZD" },
    "organism": { "id": 3358, "name": "APC - Commune d'El Milia Wilaya de Jijel" },
    "location": { "id": 20, "name": "Jijel", "type": null, "country_id": null },
    "activities": [
      { "id": 7233, "name": "[72110000] - Travaux de bâtiment" }
    ],
    "lots": [
      {
        "id": 536288,
        "name": "Etude et suivi realisation de 4 classes en extention",
        "entreprise": "Entreprise travaux batiment Beldjoudi Samir",
        "nif": null,
        "montant": "22730697.89",
        "devise": "DZD",
        "delai": 5,
        "delai_unit": "m",
        "order": 0
      }
    ]
  },
  "subscription": {
    "daily": { "limit": 100, "used": 8, "remaining": 92, "resets_at": "2026-09-24T00:00:00Z" },
    "total": { "limit": null, "used": null, "remaining": null }
  }
}
```

Response — Online consultation (status 9):

```json
{
  "success": true,
  "data": {
    "id": 1487429,
    "slug": "invitation-to-competitive-bidding-projet-ao-202677-058",
    "title": "Invitation to Competitive Bidding | Projet AO_2026/77_058",
    "publish_date": "2026-09-07",
    "ending_date": "2026-09-30T23:45:00Z",
    "is_expired": false,
    "organism": { "id": 19321, "name": "General Electric Algeria Turbines (« GEAT »)" },
    "location": { "id": 2, "name": "Algérie" },
    "status": { "id": 9, "name": "Consultation en ligne" },
    "activities": [
      { "id": 7182, "name": "[40180000] - Tubes et raccords" }
    ]
  },
  "subscription": {
    "daily": { "limit": 100, "used": 8, "remaining": 92, "resets_at": "2026-09-24T00:00:00Z" },
    "total": { "limit": null, "used": null, "remaining": null }
  }
}
```

## Error codes

| Status | Meaning |
| --- | --- |
| `401` | Missing, invalid, expired or revoked token (generic message, never distinguished). |
| `403 (api_access_disabled)` | API access disabled for this account by an administrator. |
| `403 (ability)` | Missing ability on the token. |
| `403 (subscription_inactive)` | No active subscription grants access to the catalogue. |
| `403 (quota_exceeded)` | Subscription quota exhausted for tender views. |
| `404` | Tender not found, not accessible, or invalid route. |
| `422` | Invalid request parameters (detail in errors). |
| `429` | Rate limit exceeded (30 requests/minute). |

## Rate limits

In addition to your subscription quota (which only applies to tender detail calls), a technical limit of 30 requests per minute protects the API from abuse.

This limit is shared across all /v1/data endpoints for your account (not a separate limit per endpoint). A 429 response includes the X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After headers.
