Developers

Site API#

Read and write the content of one NAMES LEGAL site over HTTPS: services, projects, blog posts, team members, events, jobs, FAQ and more. The API is part of the API access add-on of the site.

Quick start#

  1. In the console of the site, Add-ons & subscription: activate API access.
  2. Modules → API: create a key. Copy it now — it is shown only once.
  3. Call the API:
KEY="nlk_3fa9c2d1_Jx0..."
curl -H "Authorization: Api-Key $KEY" "https://ecolehorizon.names.legal/api/v1/team/?lang=fr"
{
  "count": 12,
  "next": "https://ecolehorizon.names.legal/api/v1/team/?lang=fr&page=2",
  "previous": null,
  "results": [
    {"id": 4, "first_name": "Awa", "last_name": "Diop", "position": "Mathematics teacher",
     "profile_image": "https://ecolehorizon.names.legal/media/demo/team/awa.jpg",
     "updated_at": "2026-09-20T08:12:44+02:00"}
  ]
}

Base URL#

https://<site>/api/v1/

<site> is the site's own address (ecolehorizon.names.legal or its custom domain). There is no language prefix in the path. v1 is the only version; a breaking change would come as v2 alongside it.

The root URL lists every collection. An interactive reference (Swagger) and the OpenAPI schema are available on each site:

URL Access
Interactive reference /api/v1/docs/ API key (any scope) or a console account
OpenAPI 3 schema /api/v1/schema/ API key (any scope) or a console account

Authentication#

  1. The site owner activates the API access add-on.
  2. In the console, Modules → API, they create a key and choose its scope. The full key is shown once; only a prefix and a hash are stored.
  3. Send the key with every request.
GET /api/v1/services/ HTTP/1.1
Host: ecolehorizon.names.legal
Authorization: Api-Key nlk_3fa9c2d1_Jx0...

Authorization: Bearer <key> and X-Api-Key: <key> are accepted as well. Keys look like nlk_<8 hex characters>_<secret>.

Scopes#

Scope Allows
read (default) GET on every collection
write GET, POST, PUT, PATCH

The scope applies to the whole key; there is no per-collection scope. A key can be revoked at any time from the console.

Errors you may meet#

Status Meaning
401 Missing, unknown or revoked key: {"detail": "Invalid API key."}
403 The add-on is not active, or a read key tried to write
404 /docs/ and /schema/ when the add-on is not active
405 DELETE — deleting is done from the console only
429 Rate limit exceeded
400 Validation error: {"field": ["message"]}

Language#

Translated fields are returned in one language at a time. Choose it with ?lang=:

curl -H "Authorization: Api-Key $KEY" "https://ecolehorizon.names.legal/api/v1/services/?lang=fr"

The language must be one of the languages published by the site; otherwise the answer is 400. Without lang, the site's default language is used. The response carries a Content-Language header.

Pagination, filters and sorting#

Lists are paginated by page number:

{
  "count": 42,
  "next": "https://ecolehorizon.names.legal/api/v1/team/?page=2",
  "previous": null,
  "results": [ ... ]
}
Parameter Effect
page, page_size Page number and size (default 25, maximum 100)
search Full-text search on the collection's main text fields
ordering Sort field, - for descending: ?ordering=-updated_at
field filters Exact match, for example ?category__slug=bachelor&is_featured=true
since Only entries changed since a date: ?since=2026-09-01T00:00:00Z

since makes incremental synchronisation cheap: store the time of your last call and pass it next time.

Rate limits#

  • 120 requests per minute per API key (each key has its own counter).
  • Console sessions: 120 per minute. Anonymous calls: 30 per minute.

Past the limit the API answers 429; wait and retry.

Collections#

Path Methods Notes
services/ GET, POST, PUT, PATCH Category by category_slug (optional)
projects/ GET, POST, PUT, PATCH category_slug required
blog/ GET, POST, PUT, PATCH Published posts only; category_slug required
team/ GET, POST, PUT, PATCH Active members only
testimonials/ GET, POST, PUT, PATCH Real testimonials only
publications/ GET, POST, PUT, PATCH category_slug optional
resources/ GET, POST, PUT, PATCH The file itself is managed in the console
events/ GET, POST, PUT, PATCH Dates in ISO 8601
jobs/ GET, POST, PUT, PATCH Active openings only; category_slug required
faq/ GET, POST, PUT, PATCH category_slug optional
bookable-items/ GET Requires the Appointments add-on
bookings/ POST Create a booking; needs a write key

Each entry is reachable at <collection>/<id>/. The fields of every collection are listed in the reference below.

Writing#

  • POST creates, PUT replaces, PATCH updates some fields.
  • Categories are read as an object {"id", "name", "slug"} and written with category_slug. An unknown slug is refused.
  • Images and files are read-only in the API: upload them in the console, or let the data connector download them from a URL.
  • HTML sent in rich-text fields is cleaned: scripts and event handlers are removed.
curl -X POST "https://ecolehorizon.names.legal/api/v1/faq/?lang=fr" \
  -H "Authorization: Api-Key $KEY" -H "Content-Type: application/json" \
  -d '{"question": "Quand ont lieu les inscriptions ?", "answer": "<p>Du 1er au 30 juin.</p>"}'

Booking an appointment#

curl -X POST "https://ecolehorizon.names.legal/api/v1/bookings/" \
  -H "Authorization: Api-Key $KEY" -H "Content-Type: application/json" \
  -d '{"item": 3, "customer_name": "Awa Diop", "customer_email": "awa@example.com",
       "date": "2026-10-02", "time": "14:30"}'

time is in the site's time zone. A slot that is no longer free is refused with 400. New bookings start as pending.

Recipes#

Mirror a collection incrementally#

Read everything once, then only what changed. Keep the time of your previous run and pass it as since.

Python

import time
import requests

BASE = "https://ecolehorizon.names.legal/api/v1/"
HEADERS = {"Authorization": "Api-Key nlk_3fa9c2d1_Jx0..."}

def fetch_all(collection, since=None, lang="fr"):
    url = f"{BASE}{collection}/"
    params = {"lang": lang, "page_size": 100, "ordering": "updated_at"}
    if since:
        params["since"] = since
    while url:
        response = requests.get(url, headers=HEADERS, params=params, timeout=15)
        if response.status_code == 429:
            time.sleep(int(response.headers.get("Retry-After", "5")))
            continue
        response.raise_for_status()
        data = response.json()
        yield from data["results"]
        url, params = data["next"], None   # "next" already carries the parameters

for member in fetch_all("team", since="2026-09-01T00:00:00Z"):
    print(member["id"], member["first_name"], member["last_name"])

JavaScript (Node.js 18+)

const BASE = "https://ecolehorizon.names.legal/api/v1/";
const HEADERS = { Authorization: "Api-Key nlk_3fa9c2d1_Jx0..." };

async function* fetchAll(collection, since, lang = "fr") {
  const first = new URL(`${BASE}${collection}/`);
  first.search = new URLSearchParams({ lang, page_size: "100", ordering: "updated_at",
                                       ...(since ? { since } : {}) });
  let url = first.toString();
  while (url) {
    const response = await fetch(url, { headers: HEADERS });
    if (response.status === 429) {
      const wait = Number(response.headers.get("Retry-After") || 5);
      await new Promise((resolve) => setTimeout(resolve, wait * 1000));
      continue;
    }
    if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
    const data = await response.json();
    yield* data.results;
    url = data.next;
  }
}

for await (const post of fetchAll("blog", "2026-09-01T00:00:00Z")) {
  console.log(post.id, post.title);
}

Create or update an entry#

PHP

<?php
$base = 'https://ecolehorizon.names.legal/api/v1/';
$headers = ['Authorization: Api-Key nlk_3fa9c2d1_Jx0...', 'Content-Type: application/json'];

function call($method, $url, $headers, $body = null) {
    $ch = curl_init($url);
    curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers,
                            CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15]);
    if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    $answer = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    return [$status, json_decode($answer, true)];
}

// Create an event (in French)
[$status, $event] = call('POST', $base . 'events/?lang=fr', $headers, [
    'title' => 'Journée portes ouvertes',
    'slug' => 'journee-portes-ouvertes-2026',
    'description' => '<p>Visite du campus et rencontre avec les enseignants.</p>',
    'start_date' => '2026-11-14T09:00:00+01:00',
    'end_date' => '2026-11-14T16:00:00+01:00',
    'location' => 'Campus de Genève',
]);

// Change only the location later
call('PATCH', $base . "events/{$event['id']}/?lang=fr", $headers, ['location' => 'Aula']);

lang decides which language a translated field is written to: send the same entry again with ?lang=en to add the English version.

Errors and limits in practice#

Situation What to do
429 Too Many Requests Wait the number of seconds given in the Retry-After header, then retry.
400 on write Read the body: each field lists its problems, {"category_slug": ["Object with slug=x does not exist."]}.
403 with a write request The key is read: create a read and write key.
401 The key was revoked or mistyped.
Network error or 5xx Retry with a growing delay (1 s, 2 s, 4 s…). Reads are safe to retry; for POST, check first that the entry was not created.

Reference by collection#

Generated from the code of the API: fields, access rights and query parameters are always those of the running platform. Translated fields are read and written in the language chosen with lang.

/api/v1/services/

GETPOSTPUTPATCH

  • Filters slug category__slug is_featured
  • search title description
  • ordering order title updated_at (default order)
FieldField typeAccess
id integer read only
slug string ≤ 200 read and write
title string ≤ 100 read and write required to create
description string read and write required to create
icon string ≤ 50 read and write required to create
color string ≤ 25 read and write
category object read only
category_slug slug write only
is_featured boolean read and write
order integer read and write
seo_title string ≤ 70 read and write
seo_description string ≤ 160 read and write
og_image id read only
updated_at datetime read only

/api/v1/projects/

GETPOSTPUTPATCH

  • Filters slug category__slug status is_featured
  • search title description client technologies
  • ordering order title project_date updated_at (default order)
FieldField typeAccess
id integer read only
slug string ≤ 50 read and write required to create
title string ≤ 150 read and write required to create
description string read and write required to create
detailed_description string read and write
category object read only
category_slug slug write only required to create
client string ≤ 100 read and write
status choice
completed, in_progress, on_hold, planning
read and write
project_date string ≤ 20 read and write
duration string ≤ 50 read and write
team_size string ≤ 50 read and write
technologies string ≤ 200 read and write
tags string ≤ 200 read and write
image file (URL) read only
demo_url url ≤ 200 read and write
project_url url ≤ 200 read and write
github_url url ≤ 200 read and write
is_featured boolean read and write
order integer read and write
seo_title string ≤ 70 read and write
seo_description string ≤ 160 read and write
og_image id read only
created_at datetime read only
updated_at datetime read only

/api/v1/blog/

GETPOSTPUTPATCH

  • Filters slug category__slug status tags__slug
  • search title excerpt content
  • ordering published_date title updated_at (default -published_date)
FieldField typeAccess
id integer read only
slug string ≤ 200 read and write required to create
title string ≤ 200 read and write required to create
excerpt string ≤ 500 read and write required to create
content string read and write required to create
category object read only
category_slug slug write only required to create
tags list of slug read only
featured_image file (URL) read only
published_date datetime read and write
read_time integer read and write
status choice
draft, published, featured
read and write
order integer read and write
seo_title string ≤ 70 read and write
seo_description string ≤ 160 read and write
og_image id read only
updated_at datetime read only

/api/v1/team/

GETPOSTPUTPATCH

  • Filters member_type is_featured
  • search first_name last_name position specialties
  • ordering order last_name updated_at (default order)
FieldField typeAccess
id integer read only
first_name string ≤ 100 read and write required to create
last_name string ≤ 100 read and write required to create
position string ≤ 150 read and write required to create
short_bio string ≤ 300 read and write
bio string read and write
profile_image file (URL) read only
email email ≤ 254 read and write
phone string ≤ 20 read and write
linkedin url ≤ 200 read and write
github url ≤ 200 read and write
twitter url ≤ 200 read and write
instagram url ≤ 200 read and write
facebook url ≤ 200 read and write
website url ≤ 200 read and write
specialties string ≤ 200 read and write
years_experience integer read and write
education string ≤ 200 read and write
location string ≤ 100 read and write
languages string ≤ 100 read and write
member_type choice
founder, lead, senior, developer, designer, manager, consultant, intern
read and write
is_featured boolean read and write
order integer read and write
updated_at datetime read only

/api/v1/testimonials/

GETPOSTPUTPATCH

  • Filters rating
  • search name position testimonial
  • ordering order rating updated_at (default order)
FieldField typeAccess
id integer read only
name string ≤ 100 read and write required to create
position string ≤ 100 read and write required to create
testimonial string read and write required to create
rating integer read and write
image file (URL) read only
order integer read and write
updated_at datetime read only

/api/v1/publications/

GETPOSTPUTPATCH

  • Filters slug category__slug year is_featured
  • search title authors journal abstract
  • ordering order year title updated_at (default order,-year)
FieldField typeAccess
id integer read only
slug string ≤ 200 read and write
title string ≤ 255 read and write required to create
authors string ≤ 255 read and write required to create
journal string ≤ 255 read and write
year string ≤ 10 read and write required to create
abstract string read and write
link url ≤ 200 read and write
category object read only
category_slug slug write only
is_featured boolean read and write
order integer read and write
seo_title string ≤ 70 read and write
seo_description string ≤ 160 read and write
og_image id read only
updated_at datetime read only

/api/v1/resources/

GETPOSTPUTPATCH

  • Filters slug resource_type is_free
  • search title description
  • ordering order title created_at updated_at (default order)
FieldField typeAccess
id integer read only
slug string ≤ 200 read and write required to create
title string ≤ 200 read and write required to create
description string read and write required to create
resource_type choice
guide, ebook, whitepaper, template, checklist, case_study
read and write
file file (URL) read only
file_size string read only
thumbnail file (URL) read only
is_free boolean read and write
requires_email boolean read and write
order integer read and write
seo_title string ≤ 70 read and write
seo_description string ≤ 160 read and write
og_image id read only
created_at datetime read only
updated_at datetime read only

/api/v1/events/

GETPOSTPUTPATCH

  • Filters slug event_type is_online is_featured
  • search title description location
  • ordering start_date title updated_at (default -start_date)
FieldField typeAccess
id integer read only
slug string ≤ 200 read and write required to create
title string ≤ 200 read and write required to create
description string read and write required to create
event_type choice
conference, workshop, webinar, seminar, training, meeting
read and write
start_date datetime read and write required to create
end_date datetime read and write required to create
timezone string ≤ 64 read and write
location string ≤ 200 read and write required to create
is_online boolean read and write
meeting_url url ≤ 200 read and write
registration_url url ≤ 200 read and write
registration_deadline datetime read and write
max_attendees integer read and write
featured_image file (URL) read only
is_featured boolean read and write
order integer read and write
seo_title string ≤ 70 read and write
seo_description string ≤ 160 read and write
og_image id read only
created_at datetime read only
updated_at datetime read only

/api/v1/jobs/

GETPOSTPUTPATCH

  • Filters slug category__slug contract_type remote_option experience_level is_featured is_urgent
  • search title description location skills_required
  • ordering published_date title updated_at (default -published_date)
FieldField typeAccess
id integer read only
slug string ≤ 50 read and write
title string ≤ 200 read and write required to create
description string read and write required to create
responsibilities string read and write
requirements string read and write required to create
qualifications string read and write
benefits string read and write
category object read only
category_slug slug write only required to create
location string ≤ 200 read and write required to create
contract_type choice
full_time, part_time, contract, freelance, internship, temporary
read and write
remote_option choice
onsite, remote, hybrid
read and write
experience_level choice
entry, junior, mid, senior, lead, executive
read and write
education_level string ≤ 100 read and write
languages_required string ≤ 200 read and write
skills_required string read and write
skills_preferred string read and write
tags string ≤ 500 read and write
salary_min decimal read and write
salary_max decimal read and write
salary_currency string ≤ 3 read and write
salary_period choice
hour, month, year
read and write
expected_start_date date read and write
application_deadline date read and write
is_active boolean read and write
is_featured boolean read and write
is_urgent boolean read and write
published_date datetime read only
seo_title string ≤ 70 read and write
seo_description string ≤ 160 read and write
og_image id read only
created_at datetime read only
updated_at datetime read only

/api/v1/faq/

GETPOSTPUTPATCH

  • Filters category__slug is_featured
  • search question answer
  • ordering order updated_at (default order)
FieldField typeAccess
id integer read only
question string ≤ 300 read and write required to create
answer string read and write required to create
category object read only
category_slug slug write only
is_featured boolean read and write
order integer read and write
updated_at datetime read only

/api/v1/bookable-items/

GET

FieldField typeAccess
id integer read only
label string read only
duration_minutes integer read only
price decimal read only
currency string read only
updated_at datetime read only

/api/v1/bookings/

POST

FieldField typeAccess
item integer read and write required to create
customer_name string ≤ 120 read and write required to create
customer_email email read and write required to create
customer_phone string ≤ 40 read and write
date date read and write required to create
time string read and write required to create
note string read and write

Security recommendations#

  • Keep keys on your server. Never ship a key in a mobile app or in browser code.
  • Use a read key wherever you do not need to write.
  • One key per integration: revoking one does not break the others.