Ir al contenido principal

Open platform

Developers

A read-only public API, signed webhooks, manifests verifiable offline and an embeddable widget. Everything a third party can read is already public on the site: never a participant, a volunteer, a message or an account.

Public API

The public API lives under /api/public/v1: stable, versioned, read-only. Movements, claims and charter versions, upcoming events, affiliations, published finances, fingerprint verification.

  • Read-only: GET, HEAD and OPTIONS; any write answers 405.
  • Public data only: what is visible on the site, no more and no less.
  • CORS open for reading, with no cookies or credentials: a browser calls the API without a key.
  • Cursor pagination (?limit= and ?cursor=), lists sorted by date, never by popularity; no totals.
  • ?lang=fr|en: validated, up-to-date translation if there is one, otherwise the source language. Fingerprints stay those of the source language.
  • ETag and If-None-Match (304 response); Cache-Control public, max-age=60; withdrawn content disappears from the origin immediately.

Getting a key

Without a key, the quota is low. A key gives a higher quota, with no additional access. A movement administrator creates keys in “API keys” in the movement area; a verified organisation account, in “Organisations”. The secret is shown only once.

  • Never in a request address: ?api_key= is refused (400). Use Authorization: Bearer ak_live_… or X-Api-Key.
  • Never in a page or app served to the browser: a key is meant for a server.
  • 365 days at most, 10 usable keys per owner, rotation with an overlap of up to 72 hours, immediate revocation. A movement’s key stops working if its creator is no longer an administrator of it.

Quotas

Public API quotas
CallerDefault quotaCounter
Anonymous60 requests per minuteIPv4 address or IPv6 /64 prefix
With a key600 requests per minute (6,000 at most)The key

Costly routes weigh more: a movement’s manifest counts 10 units, the widget (embed) and verification with a movement 5, the rest 1. A global ceiling protects each (429 public_route_busy) and IPv6 addresses in the same /48 share a budget.

Every response carries the quota state in the RateLimit-* headers:

RateLimit-Limit: 600
RateLimit-Remaining: 597
RateLimit-Reset: 41
RateLimit-Policy: 600;w=60
X-Arallia-Api-Version: v1

Beyond that, 429 rate_limited with Retry-After (seconds): respect it and do not retry a 4xx (except 429).

Examples

curl -s "https://arallia.fr/api/public/v1/movements/climat-rhone/claims?lang=en"
curl -s "https://arallia.fr/api/public/v1/events?near=45.76,4.84&radius_km=50&limit=20" \
  -H "Authorization: Bearer ak_live_0123456789_<64 hex>"
curl -si "https://arallia.fr/api/public/v1/movements/climat-rhone" -H 'If-None-Match: W/"0a1b…"'

Versioning

The /v1 prefix is the version. Additive changes (field, route, enum value) do not change it: tolerate unknown fields and values. A breaking change creates /v2; /v1 stays served for at least 6 months, with Deprecation then Sunset headers and a successor-version link. Every response carries X-Arallia-Api-Version.

OpenAPI schema of the public API (JSON)

The API provides no ranking and no score: do not build any. An unverified legal pack or an unconfirmed affiliation must stay flagged as such.

Webhooks

A movement administrator subscribes an https address to the movement’s events, from “Webhooks” in their area. ARALLIA sends a signed JSON POST.

Types: claims_version_published, charter_version_published, event_published, event_updated, event_cancelled, event_reinstated, official_post_published, alert_published, alert_retracted, proposal_adopted, proposal_executed (and ping for tests).

{
  "id": "0192f3e0-7c1a-7e44-9a0b-6d1e2f3a4b5c",
  "type": "event_cancelled",
  "created_at": "2026-10-05T09:12:44Z",
  "movement": { "id": "…", "slug": "climat-rhone" },
  "data": { "event_id": "…", "event_slug": "marche-pour-le-climat-lyon" }
}

The body is minimal: identifiers and public data. It says “something changed”; read the public API for the content. No order is guaranteed between events: use created_at.

Headers

X-Arallia-Event: event_cancelled
X-Arallia-Delivery: 0192f3e0-7c1a-7e44-9a0b-6d1e2f3a4b5c
X-Arallia-Signature: t=1760000000,v1=9f2c…e1,v1=4b7a…03
User-Agent: Arallia-Webhooks/1

Verify the signature (mandatory)

  1. Read the raw body, before any JSON re-formatting.
  2. Extract t and each v1 from the X-Arallia-Signature header (t=[Unix seconds],v1=[hex]).
  3. Reject if the difference between now and t exceeds 300 seconds: this is the replay protection.
  4. Compute HMAC-SHA256(secret, t + "." + body) in hexadecimal and compare it with each v1 in constant time.
  5. Deduplicate on X-Arallia-Delivery, identical on every attempt: a delivery may be retried.
import hmac, hashlib, time

def verify(header: str, body: bytes, secret: str, tolerance: int = 300) -> bool:
    parts = [p.split("=", 1) for p in header.split(",")]
    t = int(next(v for k, v in parts if k == "t"))
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, v) for k, v in parts if k == "v1")

Secret rotation

On rotation, the old secret stays valid for 24 hours by default (0 to 72 hours): during that time the header carries two v1 signatures. Accept both, deploy the new secret, then only the new one remains.

Delivery and retries

  • Answer with a 2xx code in under 5 seconds. Any other code (3xx redirects included, not followed), a timeout or a connection error is a failure.
  • 8 attempts over 22 h 36 (after 1 min, 5 min, 30 min, 1 h, 3 h, 6 h, 12 h), then abandoned. One delivery at a time per recipient.
  • After 10 consecutive failures, the subscription is disabled and the team is notified; re-enable after fixing.
  • Address: https, public domain name (no IP, no localhost, no local name), no credentials, port 443, resolved to public addresses on every delivery. A refused address is reported as “URL not allowed”.
  • Test: the “Send a test” button sends a signed ping and shows the result; the log keeps 30 days of deliveries.

Verify signatures

The platform signs with Ed25519 the fingerprint of each movement’s manifest (arallia-manifest-4), the set of its events and that of its validated translations. Verification is done offline, without taking our word for it.

Start by pinning the key: note the SHA-256 fingerprint of the signing key from a source independent of this site (platform announcement, public repository, DNS) and reject any other key. Without pinning, you can only conclude the document is consistent (valid_unpinned), never where it comes from.

  1. Fetch the signed manifest (…/manifest) and the keys (/.well-known/arallia-keys.json, revalidated on every read).
  2. Recompute the SHA-256 of the manifest’s canonical JSON and compare it with sha256 and signature.manifest_hash.
  3. Find the key by key_id, reject it if revoked, and require its fingerprint to be pinned.
  4. Verify the Ed25519 signature of the v2 message (schema, fingerprint, key_id and signed_at) and that signed_at falls within the key’s validity.
  5. Do the same for events (their absence makes the document partial) and for translations if present.
# 1. Récupérer le manifeste signé et les clés publiques
curl -s https://<site>/api/public/v1/movements/<mouvement>/manifest -o manifeste.json
curl -s https://<site>/.well-known/arallia-keys.json -o arallia-keys.json

# 2. Vérifier hors ligne, en épinglant l'empreinte de clé publiée HORS du site
arallia-admin verify-manifest manifeste.json --keys arallia-keys.json --pin <empreinte-sha256>
# code 0 : valide ET clé épinglée | 3 : valide mais clé non épinglée | 4 : partiel | 1 : invalide | 2 : non signé

States: valid (verified and key pinned) is the only one that allows writing “signed”; valid_unpinned, stale, unsigned, invalid, key_revoked, unknown_key and partial never do. The signature attests publication, not the truth of the claims.

Public signing keys · Verify a fingerprint

Widget

The widget is a ARALLIA page (/embed/[movement]) that you embed in an iframe. It shows the current official claims, their version, the signature status and upcoming events.

  • The movement enables the widget and declares the allowed origins; a page whose origin is not in the list cannot display it (frame-ancestors header).
  • No authenticated action, no cookie: “I’m taking part” is a link that opens the ARALLIA site in a new tab.
  • Options: ?lang=fr|en and ?theme=light|dark (otherwise, follow the visitor’s system).
<iframe src="https://arallia.fr/embed/mon-mouvement?lang=fr&theme=light"
  title="Revendications de Mon mouvement" width="100%" height="520"
  loading="lazy" referrerpolicy="no-referrer" style="border:0"></iframe>