A project with a board now carries its key, its address and the address of the board's public showcase in the public API, so the website can show its roadmap, releases and tickets for newcomers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6636 lines
232 KiB
YAML
6636 lines
232 KiB
YAML
openapi: 3.0.3
|
|
|
|
info:
|
|
title: Pireon Admin API
|
|
version: "1.0.0"
|
|
description: |
|
|
REST API of the admin application.
|
|
|
|
## Two APIs in one document
|
|
|
|
- **`/public/v1/*`** — unauthenticated, read-only. Consumed by the public
|
|
frontend, which renders server-side. Everything it returns is already
|
|
published on the public website.
|
|
- **`/v1/*`** — authenticated administration, described further down.
|
|
|
|
## Authentication
|
|
|
|
Every endpoint under `/v1` except `POST /v1/auth/token` requires a bearer token issued
|
|
by the **Pireon identity server** (`https://oauth.pireon.dev`), which this
|
|
application validates — or a **machine token** of its own.
|
|
|
|
Obtain a token through `POST /v1/auth/token`, which proxies the identity
|
|
server's password grant. The panel's own sign-in form uses the same grant,
|
|
so `PIREON_PASSWORD_GRANT=false` closes both. Authorization Code + PKCE
|
|
against `https://oauth.pireon.dev/oauth/authorize` remains available for
|
|
clients that prefer never to handle a password.
|
|
|
|
Machine tokens (`pmt_…`) are for scripts and integrations: a person makes
|
|
one with `POST /v1/tokens`, naming the permissions it gets — never more
|
|
than their own — and when it expires. It acts as that person within
|
|
those permissions, checked on every request, so taking a role away from
|
|
them narrows their tokens at once. It is checked here and never sent to
|
|
the identity server.
|
|
|
|
## Authorisation
|
|
|
|
The identity server establishes *who* the caller is. What they may *do* is
|
|
decided here, by the roles and permissions administered in this app's
|
|
panel. `GET /v1/me` returns the caller's effective permissions; drive your
|
|
UI from that array rather than from role names.
|
|
|
|
## Languages
|
|
|
|
Content fields (`title`, `slug`, `excerpt`, `body`, …) are translatable.
|
|
By default they are returned resolved to a single language, chosen as:
|
|
|
|
1. `?lang=xx`
|
|
2. the user's stored preference
|
|
3. the `Accept-Language` header
|
|
4. the application default (`es`)
|
|
|
|
Pass `?with=translations` to receive every language alongside, and send a
|
|
`{"es": "...", "ca": "..."}` object to write specific languages. A write
|
|
that names only some languages leaves the others untouched.
|
|
|
|
servers:
|
|
- url: /api
|
|
description: API base URL
|
|
|
|
tags:
|
|
- name: Public
|
|
description: |
|
|
Unauthenticated, read-only endpoints for the public frontend.
|
|
|
|
Records are addressed **by slug within the active locale**, because that
|
|
is the access pattern a server-rendered frontend has: it receives a URL,
|
|
not an id. A slug only resolves in the language it belongs to, so one
|
|
record never answers on two addresses.
|
|
|
|
Every single-record response carries `alternates` — the same record's
|
|
path in each language that actually has a translation. Only this API
|
|
knows that `/proyectos/mi-web` and `/ca/projectes/el-meu-web` are one
|
|
thing, so without it the frontend cannot emit correct `hreflang` tags.
|
|
Locales with no translation are omitted rather than pointing at a URL
|
|
that would 404.
|
|
|
|
Responses carry `ETag`, `Cache-Control: s-maxage`,
|
|
`stale-while-revalidate` and `Vary: Accept-Language`. Send
|
|
`If-None-Match` to revalidate cheaply, and watch `content_version` from
|
|
`/site` to know when a cached copy is genuinely out of date.
|
|
|
|
Unpublished records can be reached two ways: the shared preview secret
|
|
(`X-Preview-Token`), which unlocks everything, or a signed link generated
|
|
in the panel for one record (`?preview=…`), which expires and unlocks
|
|
only that one.
|
|
- name: Submissions
|
|
description: |
|
|
The public API's only write endpoints: the two forms the site offers.
|
|
|
|
They carry their own throttle, a honeypot and an off switch, because
|
|
they are the one place a stranger can put a row in the database. What
|
|
arrives lands in the panel's inbox, and the people who can read it are
|
|
notified.
|
|
- name: Telemetry
|
|
description: |
|
|
The frontend reporting back, rather than asking for something.
|
|
|
|
Also a write endpoint, but the author is the frontend itself and not a
|
|
visitor, so unlike the submissions it holds a shared secret
|
|
(`X-Ingest-Token`) instead of a honeypot. An unset secret disables the
|
|
endpoint; it never opens it.
|
|
|
|
Two things arrive here: addresses that failed, which are the raw material
|
|
of a redirect, and records that were read, which is the only way this
|
|
side can know whether anything it publishes is opened.
|
|
|
|
Neither carries anything about a person — no address, no session, no user
|
|
agent, no time of day.
|
|
- name: Auth
|
|
description: Token exchange and caller identity
|
|
- name: Pages
|
|
description: Translatable page content
|
|
- name: Categories
|
|
description: Translatable page categories
|
|
- name: Projects
|
|
description: The collective's work — translatable, with its team and stack
|
|
- name: Members
|
|
description: The people in the collective
|
|
- name: Services
|
|
description: What the collective offers
|
|
- name: Clients
|
|
description: The organisations the work was for
|
|
- name: Testimonials
|
|
description: What the people the collective worked with said
|
|
- name: Talks
|
|
description: Talks given elsewhere, by members of the collective
|
|
- name: Calls
|
|
description: Open calls for contributors
|
|
- name: Applications
|
|
description: The inbox — what people sent from the public site
|
|
- name: Technologies
|
|
description: What the collective uses, and its place on the tech radar
|
|
- name: Redirects
|
|
description: Old addresses, and the missing ones the frontend reported
|
|
- name: Repositories
|
|
description: The collective's git repositories and their synced numbers
|
|
- name: Settings
|
|
description: Runtime settings and the public site's maintenance mode
|
|
- name: History
|
|
description: Who changed what and when, and undoing a change
|
|
- name: Reviews
|
|
description: Review, publication and preview links
|
|
- name: Health
|
|
description: What is wrong with the site, and what is left to translate
|
|
- name: Batch
|
|
description: Several operations in one, and importing records from JSON or CSV
|
|
- name: Webhooks
|
|
description: Telling other systems what happened here
|
|
- name: Audit
|
|
description: Who did what, when, and how they came in
|
|
- name: Users
|
|
description: Local user administration
|
|
- name: Media
|
|
description: The media library, and images uploaded for a record to point at
|
|
- name: Roles
|
|
description: Local authorisation model (read-only)
|
|
|
|
security:
|
|
- bearerAuth: []
|
|
|
|
paths:
|
|
|
|
/public/v1/site:
|
|
get:
|
|
tags: [Public]
|
|
summary: Locales and headline numbers
|
|
description: What the frontend needs once on any page.
|
|
security: []
|
|
responses:
|
|
"200":
|
|
description: Site metadata
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: object
|
|
properties:
|
|
locales:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
code: { type: string, example: ca }
|
|
name: { type: string, example: Català }
|
|
flag: { type: string }
|
|
is_default: { type: boolean }
|
|
default_locale: { type: string, example: es }
|
|
content_version:
|
|
type: integer
|
|
description: |
|
|
Moves whenever published content does. Keep it as part
|
|
of your cache key and everything downstream goes stale
|
|
the moment the site really changed, rather than when a
|
|
timer says so.
|
|
submissions_enabled:
|
|
type: boolean
|
|
description: |
|
|
Whether the forms are accepting messages. Hide them
|
|
when false rather than rendering one that would be
|
|
refused on submit.
|
|
stats:
|
|
type: object
|
|
properties:
|
|
projects: { type: integer }
|
|
active_projects: { type: integer }
|
|
members: { type: integer }
|
|
open_calls: { type: integer }
|
|
good_first_issues: { type: integer }
|
|
stars: { type: integer }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/slugs:
|
|
get:
|
|
tags: [Public]
|
|
summary: Every public URL, grouped by type and locale
|
|
description: |
|
|
One request instead of paging through every collection — this is what
|
|
the frontend builds its sitemap and route table from.
|
|
security: []
|
|
responses:
|
|
"200":
|
|
description: All published URLs
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: object
|
|
additionalProperties:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
updated_at: { type: string, format: date-time }
|
|
alternates:
|
|
type: object
|
|
additionalProperties: { type: string }
|
|
example:
|
|
es: /proyectos/mi-web
|
|
ca: /ca/projectes/el-meu-web
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/redirects:
|
|
get:
|
|
tags: [Public]
|
|
summary: URLs that used to work
|
|
description: |
|
|
Every enabled redirect, so the frontend can answer a 301 instead of a
|
|
404. Most were written by the panel itself the moment somebody renamed
|
|
a slug — the only moment the old address was still known.
|
|
|
|
Returned whole rather than looked up one at a time: there are never
|
|
many, and an SSR frontend would otherwise call this API on every 404 it
|
|
serves.
|
|
security: []
|
|
responses:
|
|
"200":
|
|
description: Enabled redirects
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
from: { type: string, example: /proyectos/nombre-viejo }
|
|
to: { type: string, example: /proyectos/nombre-nuevo }
|
|
status: { type: integer, enum: [301, 302] }
|
|
locale:
|
|
type: string
|
|
nullable: true
|
|
description: Null applies in every language.
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/search:
|
|
get:
|
|
tags: [Public]
|
|
summary: Search the site
|
|
description: |
|
|
What the site shows that matches some words, in the request's
|
|
language, best match first: published pages, projects and services,
|
|
open calls, public members, and technologies in use.
|
|
|
|
Full-text in production — words are stemmed in the language searched,
|
|
so "proyectos" finds "proyecto", and a title match ranks above one in
|
|
the body. `path` is where the result lives on the site; null for a
|
|
technology, which has no address on this side.
|
|
security: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- name: q
|
|
in: query
|
|
required: true
|
|
schema: { type: string, minLength: 2, maxLength: 120 }
|
|
- name: type[]
|
|
in: query
|
|
description: "Limit to some kinds: `page`, `project`, `service`, `call`, `member`, `technology`."
|
|
schema:
|
|
type: array
|
|
items: { type: string, enum: [page, project, service, call, member, technology] }
|
|
- name: limit
|
|
in: query
|
|
schema: { type: integer, minimum: 1, maximum: 50, default: 20 }
|
|
responses:
|
|
"200":
|
|
description: Results
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicSearchResult" }
|
|
meta:
|
|
type: object
|
|
properties:
|
|
query: { type: string }
|
|
locale: { type: string }
|
|
count: { type: integer }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/tags:
|
|
get:
|
|
tags: [Public]
|
|
summary: Blog tags in use
|
|
description: Tags on something published, with how many posts each has in the request's language. Most used first.
|
|
security: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
responses:
|
|
"200":
|
|
description: The tags
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
name: { type: string }
|
|
slug: { type: string }
|
|
count: { type: integer }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/projects:
|
|
get:
|
|
tags: [Public]
|
|
summary: List published projects
|
|
security: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: status
|
|
in: query
|
|
schema: { type: string, enum: [idea, active, paused, completed, archived] }
|
|
- name: featured
|
|
in: query
|
|
schema: { type: boolean }
|
|
- name: needs_help
|
|
in: query
|
|
description: Only projects looking for contributors.
|
|
schema: { type: boolean }
|
|
- name: technology
|
|
in: query
|
|
description: Technology slug; repeat for several.
|
|
schema: { type: string }
|
|
- name: member
|
|
in: query
|
|
schema: { type: string }
|
|
- name: translated
|
|
in: query
|
|
description: Exclude projects with no translation in the active locale.
|
|
schema: { type: boolean }
|
|
responses:
|
|
"200":
|
|
description: Paginated projects
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicProject" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/projects/{slug}:
|
|
get:
|
|
tags: [Public]
|
|
summary: A single project, with its write-up
|
|
description: |
|
|
Adds the long-form fields the listing omits: `description`, `decisions`
|
|
and `postmortem`, plus members, open calls and live repository stats.
|
|
security: []
|
|
parameters:
|
|
- name: slug
|
|
in: path
|
|
required: true
|
|
description: The slug in the active locale.
|
|
schema: { type: string }
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/PreviewToken"
|
|
responses:
|
|
"200":
|
|
description: The project
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/PublicProject" }
|
|
"404": { description: No project with that slug in that language }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/members:
|
|
get:
|
|
tags: [Public]
|
|
summary: List public members
|
|
security: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- name: include_past
|
|
in: query
|
|
description: Include people who have left, for an alumni section.
|
|
schema: { type: boolean }
|
|
- name: technology
|
|
in: query
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated members
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicMember" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/members/{slug}:
|
|
get:
|
|
tags: [Public]
|
|
summary: A single member, with their projects
|
|
security: []
|
|
parameters:
|
|
- name: slug
|
|
in: path
|
|
required: true
|
|
schema: { type: string }
|
|
- $ref: "#/components/parameters/Lang"
|
|
responses:
|
|
"200":
|
|
description: The member
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/PublicMember" }
|
|
"404": { description: Not found }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/calls:
|
|
get:
|
|
tags: [Public]
|
|
summary: Open calls for contributors
|
|
description: |
|
|
Excludes closed calls, calls outside their window, and calls attached
|
|
to an unpublished project.
|
|
security: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- name: technology
|
|
in: query
|
|
schema: { type: string }
|
|
- name: max_hours_per_week
|
|
in: query
|
|
description: '"I have four hours a week" — the filter people actually apply.'
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: Paginated calls
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicCall" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/calls/{slug}:
|
|
get:
|
|
tags: [Public]
|
|
summary: A single open call
|
|
security: []
|
|
parameters:
|
|
- name: slug
|
|
in: path
|
|
required: true
|
|
schema: { type: string }
|
|
- $ref: "#/components/parameters/Lang"
|
|
responses:
|
|
"200":
|
|
description: The call
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/PublicCall" }
|
|
"404": { description: Not found or no longer open }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/calls/{slug}/apply:
|
|
post:
|
|
tags: [Submissions]
|
|
summary: Answer a call
|
|
description: |
|
|
The public API's only write endpoints are this one and
|
|
`/public/v1/contact`.
|
|
|
|
Only an **open** call accepts applications; anything else answers 404,
|
|
because letting someone write a message for a position that is already
|
|
filled wastes their time.
|
|
|
|
Submitting the same address to the same call twice inside the
|
|
deduplication window (24 h by default) answers **200** with
|
|
`duplicate: true` and stores nothing further: a double-clicked submit
|
|
button is not a second person.
|
|
|
|
Throttling is per e-mail address plus a site-wide hourly ceiling —
|
|
deliberately not per IP, since with an SSR frontend every submission
|
|
arrives from the same address.
|
|
security: []
|
|
parameters:
|
|
- name: slug
|
|
in: path
|
|
required: true
|
|
schema: { type: string }
|
|
- $ref: "#/components/parameters/Lang"
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Submission" }
|
|
responses:
|
|
"201": { $ref: "#/components/responses/SubmissionAccepted" }
|
|
"200":
|
|
description: Recognised as a repeat of a submission already stored
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/SubmissionReceipt" }
|
|
"404": { description: No open call with that slug in this language }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
"429": { description: Too many submissions from this address }
|
|
"503":
|
|
description: |
|
|
The public site is in maintenance, or submissions are switched off
|
|
(`SITE_ACCEPT_SUBMISSIONS=false`). The frontend can tell the two
|
|
apart by the `maintenance` and `submissions_enabled` keys.
|
|
|
|
/public/v1/contact:
|
|
post:
|
|
tags: [Submissions]
|
|
summary: Write in without a specific call
|
|
description: |
|
|
The general form: somebody offering time with no call in mind, or an
|
|
association asking for help. `kind` chooses between the two, and
|
|
`organisation` becomes required when it is `request`.
|
|
security: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
allOf:
|
|
- $ref: "#/components/schemas/Submission"
|
|
- type: object
|
|
properties:
|
|
kind:
|
|
type: string
|
|
enum: [contribute, request]
|
|
default: contribute
|
|
responses:
|
|
"201": { $ref: "#/components/responses/SubmissionAccepted" }
|
|
"200":
|
|
description: Recognised as a repeat of a submission already stored
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/SubmissionReceipt" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
"429": { description: Too many submissions from this address }
|
|
"503": { description: "Maintenance, or submissions switched off" }
|
|
|
|
/public/v1/telemetry/not-found:
|
|
post:
|
|
tags: [Telemetry]
|
|
summary: Report addresses the frontend could not resolve
|
|
description: |
|
|
Redirects were only ever written from this side: renaming a slug leaves
|
|
a rule behind, because that is the one moment the old address is still
|
|
known. Every other broken link — the previous website's, one printed on
|
|
paper, another site linking a page that moved years ago — fails on the
|
|
frontend, in front of a visitor, and never reaches the panel.
|
|
|
|
This is where the frontend says so. Reports land in
|
|
**Administration → Missing URLs**, where one becomes a redirect with a
|
|
single action.
|
|
|
|
Accepts either a single report or a batch of up to
|
|
`SITE_TELEMETRY_MAX_BATCH` (50 by default). Paths are normalised
|
|
exactly as redirects are — trailing slashes and query strings are
|
|
stripped — and stored as one row per `(path, locale)` with a hit count,
|
|
so a crawler costs one row rather than one per request.
|
|
|
|
Obvious scans (`/wp-admin`, `/.env`, anything ending in `.php`) are
|
|
dropped rather than stored. The response says how many of the reports
|
|
were kept, so the frontend can tell its filter from this one.
|
|
|
|
Not affected by maintenance mode: a 404 during a deploy is still a 404,
|
|
and those are the most interesting ones.
|
|
security:
|
|
- ingestToken: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/IngestToken"
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
oneOf:
|
|
- $ref: "#/components/schemas/MissingUrlReport"
|
|
- type: object
|
|
required: [hits]
|
|
properties:
|
|
hits:
|
|
type: array
|
|
minItems: 1
|
|
maxItems: 50
|
|
items: { $ref: "#/components/schemas/MissingUrlReport" }
|
|
responses:
|
|
"202":
|
|
description: |
|
|
Reports accepted. Nothing here is a resource the caller owns, and a
|
|
report merged into an existing count or dropped as noise is still
|
|
successful — hence 202 rather than 201.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: object
|
|
properties:
|
|
received: { type: integer, example: 3 }
|
|
recorded: { type: integer, example: 1 }
|
|
"403":
|
|
description: |
|
|
Missing or wrong `X-Ingest-Token`, or reporting is disabled because
|
|
`SITE_INGEST_TOKEN` is unset.
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
"429": { description: Too many reports from this address }
|
|
|
|
/public/v1/telemetry/views:
|
|
post:
|
|
tags: [Telemetry]
|
|
summary: Report what was read
|
|
description: |
|
|
The panel can say what exists and what is published. It has never been
|
|
able to say whether any of it gets opened — which is the fact behind
|
|
most editorial decisions: what to translate next, whether the featured
|
|
project is one anybody looks at, whether a page earns the work of
|
|
keeping it accurate.
|
|
|
|
Reports are addressed the way the frontend already thinks: it fetched
|
|
`/public/v1/projects/mi-web` to draw the page, so it reports a **type**
|
|
and a **slug** rather than parsing its own URL back apart. A slug that
|
|
no longer resolves is not counted — that is a 404, and
|
|
`/telemetry/not-found` is where those belong.
|
|
|
|
**There is no visitor in this.** No address, no session, no user agent,
|
|
no referrer, no time of day. What is stored is one row per record, per
|
|
language, per day, holding a number — enough for the questions above
|
|
and not enough to reconstruct anybody. Reading figures are kept for
|
|
400 days and then pruned.
|
|
|
|
Accepts a single report or a batch of up to
|
|
`SITE_TELEMETRY_MAX_BATCH`.
|
|
security:
|
|
- ingestToken: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/IngestToken"
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
oneOf:
|
|
- $ref: "#/components/schemas/ViewReport"
|
|
- type: object
|
|
required: [views]
|
|
properties:
|
|
views:
|
|
type: array
|
|
minItems: 1
|
|
maxItems: 50
|
|
items: { $ref: "#/components/schemas/ViewReport" }
|
|
responses:
|
|
"202":
|
|
description: |
|
|
Reports accepted. `recorded` counts the ones that named something
|
|
this API still serves.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: object
|
|
properties:
|
|
received: { type: integer, example: 2 }
|
|
recorded: { type: integer, example: 2 }
|
|
"403":
|
|
description: |
|
|
Missing or wrong `X-Ingest-Token`, or reporting is disabled because
|
|
`SITE_INGEST_TOKEN` is unset.
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
"429": { description: Too many reports from this address }
|
|
|
|
/public/v1/services:
|
|
get:
|
|
tags: [Public]
|
|
summary: What the collective offers
|
|
description: |
|
|
The counterpart to a project: one says what has been built, this says
|
|
what somebody would be asking for. Ordered as arranged in the panel.
|
|
security: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- name: featured
|
|
in: query
|
|
schema: { type: boolean }
|
|
responses:
|
|
"200":
|
|
description: Published services
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicService" }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/services/{slug}:
|
|
get:
|
|
tags: [Public]
|
|
summary: A single service
|
|
description: |
|
|
Carries the work that backs the offer and what people said about it —
|
|
a service page with neither is a promise.
|
|
security: []
|
|
parameters:
|
|
- name: slug
|
|
in: path
|
|
required: true
|
|
schema: { type: string }
|
|
- $ref: "#/components/parameters/Lang"
|
|
responses:
|
|
"200":
|
|
description: The service
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/PublicService" }
|
|
"404": { description: "Not found in this language, or not published" }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/clients:
|
|
get:
|
|
tags: [Public]
|
|
summary: The wall of logos
|
|
description: |
|
|
Organisations the work was for. Anyone who asked not to appear is
|
|
absent whatever else is true of them, and by default so is anyone with
|
|
no published project behind them — a logo the site could not justify
|
|
showing.
|
|
security: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- name: with_projects
|
|
in: query
|
|
description: Set to 0 to include clients with nothing published yet.
|
|
schema: { type: boolean, default: true }
|
|
responses:
|
|
"200":
|
|
description: Public clients
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicClient" }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/testimonials:
|
|
get:
|
|
tags: [Public]
|
|
summary: What other people said
|
|
description: |
|
|
Attached to a client, a project or a service rather than to a page, so
|
|
ask for the ones belonging to whatever you are rendering.
|
|
security: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- name: client
|
|
in: query
|
|
description: Client slug.
|
|
schema: { type: string }
|
|
- name: service
|
|
in: query
|
|
description: Service slug, in the active language.
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Public testimonials
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicTestimonial" }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/talks:
|
|
get:
|
|
tags: [Public]
|
|
summary: Talks given elsewhere
|
|
security: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- name: with_video
|
|
in: query
|
|
schema: { type: boolean }
|
|
- name: limit
|
|
in: query
|
|
schema: { type: integer, default: 50, maximum: 100 }
|
|
responses:
|
|
"200":
|
|
description: Public talks, newest first
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicTalk" }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/pages:
|
|
get:
|
|
tags: [Public]
|
|
summary: List published pages
|
|
security: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: category
|
|
in: query
|
|
schema: { type: string }
|
|
- name: tag
|
|
in: query
|
|
description: A tag's slug in the active language.
|
|
schema: { type: string }
|
|
- name: featured
|
|
in: query
|
|
schema: { type: boolean }
|
|
responses:
|
|
"200":
|
|
description: Paginated pages
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicPage" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/pages/{slug}:
|
|
get:
|
|
tags: [Public]
|
|
summary: A single published page
|
|
security: []
|
|
parameters:
|
|
- name: slug
|
|
in: path
|
|
required: true
|
|
schema: { type: string }
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/PreviewToken"
|
|
responses:
|
|
"200":
|
|
description: The page
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/PublicPage" }
|
|
"404": { description: Not found }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/technologies:
|
|
get:
|
|
tags: [Public]
|
|
summary: Technologies in use
|
|
security: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- name: category
|
|
in: query
|
|
schema: { type: string, enum: [language, framework, datastore, infrastructure, tool, practice] }
|
|
- name: in_use
|
|
in: query
|
|
description: Defaults to true — only technologies attached to a project.
|
|
schema: { type: boolean }
|
|
responses:
|
|
"200":
|
|
description: Technologies
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicTechnology" }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/radar:
|
|
get:
|
|
tags: [Public]
|
|
summary: The tech radar
|
|
description: |
|
|
Technologies placed on a ring — adopt, trial, assess or hold — with the
|
|
note explaining why. Technologies with no ring are omitted.
|
|
security: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
responses:
|
|
"200":
|
|
description: Radar entries
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicTechnology" }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/public/v1/good-first-issues:
|
|
get:
|
|
tags: [Public]
|
|
summary: Newcomer-friendly open issues across all repositories
|
|
description: |
|
|
Mirrored from the git provider and refreshed hourly. Claimed issues,
|
|
archived repositories and repositories hidden from the site are
|
|
excluded — an issue somebody already took is not an invitation.
|
|
security: []
|
|
parameters:
|
|
- name: limit
|
|
in: query
|
|
schema: { type: integer, default: 20, maximum: 100 }
|
|
- name: language
|
|
in: query
|
|
description: Filter by the repository's primary language.
|
|
schema: { type: string, example: PHP }
|
|
- name: include_help_wanted
|
|
in: query
|
|
schema: { type: boolean, default: true }
|
|
responses:
|
|
"200":
|
|
description: Open issues
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicIssue" }
|
|
"503": { $ref: "#/components/responses/Maintenance" }
|
|
|
|
/v1/auth/token:
|
|
post:
|
|
tags: [Auth]
|
|
summary: Exchange credentials or a refresh token for an access token
|
|
description: |
|
|
Proxies the identity server's `password` and `refresh_token` grants so
|
|
first-party clients need not embed the OAuth server's URL or secret.
|
|
Rate limited to 6 requests per minute per account and address, and 30
|
|
per address overall. Browser clients should use Authorization Code +
|
|
PKCE instead.
|
|
security: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
oneOf:
|
|
- $ref: "#/components/schemas/PasswordGrantRequest"
|
|
- $ref: "#/components/schemas/RefreshGrantRequest"
|
|
responses:
|
|
"200":
|
|
description: Token issued
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/TokenResponse" }
|
|
"401": { description: Credentials rejected by the identity server }
|
|
"403": { description: Account inactive or awaiting approval }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/me:
|
|
get:
|
|
tags: [Auth]
|
|
summary: The current caller, with their effective local permissions
|
|
responses:
|
|
"200":
|
|
description: The authenticated user
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/User" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/v1/tokens:
|
|
get:
|
|
tags: [Auth]
|
|
summary: List my machine tokens
|
|
description: Newest first, revoked and expired ones included. A super admin may ask for everyone's with `all`. Not available to a machine token.
|
|
parameters:
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: all
|
|
in: query
|
|
description: "Everyone's tokens. Super admins only."
|
|
schema: { type: boolean }
|
|
responses:
|
|
"200":
|
|
description: Paginated tokens
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ApiTokenCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
post:
|
|
tags: [Auth]
|
|
summary: Make a machine token
|
|
description: |
|
|
`abilities` are permission names — `pages.update` — or a whole
|
|
resource — `pages.*` — and only ones the caller holds. It expires
|
|
after 90 days unless `expires_at` says otherwise, a year at most. The
|
|
token itself is in the answer's `token`, and nowhere else ever again.
|
|
Not available to a machine token.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name, abilities]
|
|
properties:
|
|
name: { type: string, maxLength: 120, example: Despliegue continuo }
|
|
abilities: { type: array, items: { type: string }, example: ["technologies.*", "media.view_any"] }
|
|
expires_at: { type: string, format: date-time }
|
|
responses:
|
|
"201":
|
|
description: Token made
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/ApiToken" }
|
|
token: { type: string, description: The token. Shown once. }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/tokens/{token}:
|
|
delete:
|
|
tags: [Auth]
|
|
summary: Revoke a machine token
|
|
description: It stops working at once and stays listed as revoked. Your own, or anyone's for a super admin.
|
|
parameters:
|
|
- name: token
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"204": { description: Revoked }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/pages:
|
|
get:
|
|
tags: [Pages]
|
|
summary: List pages
|
|
description: |
|
|
Callers without the `pages.publish` permission only ever see published
|
|
pages.
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: status
|
|
in: query
|
|
schema: { type: string, enum: [draft, review, published, archived] }
|
|
- name: category_id
|
|
in: query
|
|
schema: { type: integer }
|
|
- name: is_featured
|
|
in: query
|
|
schema: { type: boolean }
|
|
- name: search
|
|
in: query
|
|
description: Matches title and excerpt within the active language only.
|
|
schema: { type: string }
|
|
- name: translated_in
|
|
in: query
|
|
description: Only pages that actually carry a translation for this locale.
|
|
schema: { type: string, enum: [es, en, ca] }
|
|
- name: trashed
|
|
in: query
|
|
schema: { type: boolean }
|
|
- name: sort
|
|
in: query
|
|
description: 'Prefix with "-" for descending, e.g. `-published_at`.'
|
|
schema:
|
|
type: string
|
|
enum: [title, created_at, updated_at, published_at, status,
|
|
-title, -created_at, -updated_at, -published_at, -status]
|
|
responses:
|
|
"200":
|
|
description: Paginated pages
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/PageCollection" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
post:
|
|
tags: [Pages]
|
|
summary: Create a page
|
|
description: >
|
|
Requires `pages.create`. Setting `status` to `published` additionally
|
|
requires `pages.publish`.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/PageWrite" }
|
|
responses:
|
|
"201":
|
|
description: Page created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Page" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/pages/{page}:
|
|
parameters:
|
|
- name: page
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Pages]
|
|
summary: Show a page
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
responses:
|
|
"200":
|
|
description: The page
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Page" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { description: Not found }
|
|
patch:
|
|
tags: [Pages]
|
|
summary: Update a page
|
|
description: |
|
|
Partial update. Translatable fields merge into the existing map, so
|
|
sending only `{"title": {"ca": "..."}}` leaves Spanish and English
|
|
intact. Authors may always edit their own unpublished pages.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/PageWrite" }
|
|
responses:
|
|
"200":
|
|
description: Page updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Page" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
delete:
|
|
tags: [Pages]
|
|
summary: Soft-delete a page
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/pages/{page}/restore:
|
|
post:
|
|
tags: [Pages]
|
|
summary: Restore a soft-deleted page
|
|
parameters:
|
|
- name: page
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: Page restored
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Page" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/categories:
|
|
get:
|
|
tags: [Categories]
|
|
summary: List categories
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: is_visible
|
|
in: query
|
|
schema: { type: boolean }
|
|
- name: search
|
|
in: query
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated categories
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/CategoryCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
post:
|
|
tags: [Categories]
|
|
summary: Create a category
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/CategoryWrite" }
|
|
responses:
|
|
"201":
|
|
description: Category created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Category" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/categories/{category}:
|
|
parameters:
|
|
- name: category
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Categories]
|
|
summary: Show a category
|
|
responses:
|
|
"200":
|
|
description: The category
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Category" }
|
|
patch:
|
|
tags: [Categories]
|
|
summary: Update a category
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/CategoryWrite" }
|
|
responses:
|
|
"200":
|
|
description: Category updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Category" }
|
|
delete:
|
|
tags: [Categories]
|
|
summary: Soft-delete a category
|
|
description: Refused with 403 while the category still holds pages.
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/tags:
|
|
get:
|
|
tags: [Categories]
|
|
summary: List blog tags
|
|
description: Every tag, unpaginated, with how many pages carry it. Kept by whoever keeps the categories.
|
|
responses:
|
|
"200":
|
|
description: The tags
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Tag" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
post:
|
|
tags: [Categories]
|
|
summary: Create a blog tag
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/TagWrite" }
|
|
responses:
|
|
"201":
|
|
description: Tag created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Tag" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/tags/{tag}:
|
|
parameters:
|
|
- name: tag
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
patch:
|
|
tags: [Categories]
|
|
summary: Update a blog tag
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/TagWrite" }
|
|
responses:
|
|
"200":
|
|
description: Tag updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Tag" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
delete:
|
|
tags: [Categories]
|
|
summary: Delete a blog tag
|
|
description: The pages keep existing; they just stop carrying it.
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/technologies:
|
|
get:
|
|
tags: [Technologies]
|
|
summary: List technologies
|
|
description: Ordered by `position`, then name.
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: category
|
|
in: query
|
|
schema: { type: string, enum: [language, framework, datastore, infrastructure, tool, practice] }
|
|
- name: radar_ring
|
|
in: query
|
|
description: '`none` for the technologies kept off the radar.'
|
|
schema: { type: string, enum: [adopt, trial, assess, hold, none] }
|
|
- name: search
|
|
in: query
|
|
description: Matches the name.
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated technologies
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/TechnologyCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
post:
|
|
tags: [Technologies]
|
|
summary: Create a technology
|
|
description: |
|
|
For a logo, upload it first with `POST /v1/media` and `for:
|
|
technologies`, and send the `path` it answers with as `logo`.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/TechnologyWrite" }
|
|
responses:
|
|
"201":
|
|
description: Technology created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Technology" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/technologies/{technology}:
|
|
parameters:
|
|
- name: technology
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Technologies]
|
|
summary: Show a technology
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
responses:
|
|
"200":
|
|
description: The technology
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Technology" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { description: Not found }
|
|
patch:
|
|
tags: [Technologies]
|
|
summary: Update a technology
|
|
description: |
|
|
Partial update. `description` and `radar_note` merge into the existing
|
|
translations; send `radar_ring: null` to take it off the radar.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/TechnologyWrite" }
|
|
responses:
|
|
"200":
|
|
description: Technology updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Technology" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
delete:
|
|
tags: [Technologies]
|
|
summary: Delete a technology
|
|
description: Refused with 403 while a project, member or call still uses it.
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/projects:
|
|
get:
|
|
tags: [Projects]
|
|
summary: List projects
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: status
|
|
in: query
|
|
schema: { type: string, enum: [idea, active, paused, completed, archived] }
|
|
- name: search
|
|
in: query
|
|
description: Matches the title within the active language only.
|
|
schema: { type: string }
|
|
- name: trashed
|
|
in: query
|
|
schema: { type: boolean }
|
|
responses:
|
|
"200":
|
|
description: Paginated projects
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ProjectCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
post:
|
|
tags: [Projects]
|
|
summary: Create a project
|
|
description: |
|
|
Relations are sent as ids and replaced whole when present:
|
|
`technology_ids`, `service_ids`, and `members` with each one's role.
|
|
`published_at` and `unpublish_at` require `projects.publish`. For a
|
|
cover, upload it with `POST /v1/media` and `for: projects`.
|
|
|
|
With `taskstream: true` the project also gets a **public** board in
|
|
TaskStream, the collective's issue tracker, and the answer carries its
|
|
link in `taskstream_url`. Its key is `taskstream_key`, or one made from
|
|
the slug in the default language (one word: its first ten characters;
|
|
several: their initials). The board is opened first: if TaskStream
|
|
refuses it (a key in use, say), nothing is created here either and the
|
|
reason comes back as a validation error. It is opened as the caller,
|
|
whose Pireon token is passed on; a machine token cannot ask for one
|
|
unless the server has a `TASKSTREAM_TOKEN` of its own.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ProjectWrite" }
|
|
responses:
|
|
"201":
|
|
description: Project created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Project" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
"502":
|
|
description: TaskStream did not answer; nothing was created.
|
|
|
|
/v1/projects/{project}:
|
|
parameters:
|
|
- name: project
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Projects]
|
|
summary: Show a project
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
responses:
|
|
"200":
|
|
description: The project
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Project" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { description: Not found }
|
|
patch:
|
|
tags: [Projects]
|
|
summary: Update a project
|
|
description: Partial update. Translatable fields merge into the existing map.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ProjectWrite" }
|
|
responses:
|
|
"200":
|
|
description: Project updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Project" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
delete:
|
|
tags: [Projects]
|
|
summary: Soft-delete a project
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/projects/{project}/restore:
|
|
post:
|
|
tags: [Projects]
|
|
summary: Restore a soft-deleted project
|
|
parameters:
|
|
- name: project
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: Project restored
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Project" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/members:
|
|
get:
|
|
tags: [Members]
|
|
summary: List members
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: search
|
|
in: query
|
|
description: Matches the name.
|
|
schema: { type: string }
|
|
- name: is_public
|
|
in: query
|
|
schema: { type: boolean }
|
|
- name: trashed
|
|
in: query
|
|
schema: { type: boolean }
|
|
responses:
|
|
"200":
|
|
description: Paginated members
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/MemberCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
post:
|
|
tags: [Members]
|
|
summary: Create a member
|
|
description: |
|
|
`links` is a label → URL map. `technology_ids` is replaced whole when
|
|
present; which projects someone is on is set from the project, where
|
|
the role belongs. For a photo, upload it with `POST /v1/media` and
|
|
`for: members`. Someone may edit their own profile without
|
|
`members.update`.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/MemberWrite" }
|
|
responses:
|
|
"201":
|
|
description: Member created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Member" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/members/{member}:
|
|
parameters:
|
|
- name: member
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Members]
|
|
summary: Show a member
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
responses:
|
|
"200":
|
|
description: The member
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Member" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { description: Not found }
|
|
patch:
|
|
tags: [Members]
|
|
summary: Update a member
|
|
description: Partial update. Translatable fields merge into the existing map.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/MemberWrite" }
|
|
responses:
|
|
"200":
|
|
description: Member updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Member" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
delete:
|
|
tags: [Members]
|
|
summary: Soft-delete a member
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/members/{member}/restore:
|
|
post:
|
|
tags: [Members]
|
|
summary: Restore a soft-deleted member
|
|
parameters:
|
|
- name: member
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: Member restored
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Member" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/services:
|
|
get:
|
|
tags: [Services]
|
|
summary: List services
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: is_published
|
|
in: query
|
|
schema: { type: boolean }
|
|
- name: search
|
|
in: query
|
|
description: Matches the title within the active language only.
|
|
schema: { type: string }
|
|
- name: trashed
|
|
in: query
|
|
schema: { type: boolean }
|
|
responses:
|
|
"200":
|
|
description: Paginated services
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ServiceCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
post:
|
|
tags: [Services]
|
|
summary: Create a service
|
|
description: |
|
|
`technology_ids` and `project_ids` are replaced whole when present.
|
|
`is_published`, `published_at` and `unpublish_at` require
|
|
`services.publish`.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ServiceWrite" }
|
|
responses:
|
|
"201":
|
|
description: Service created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Service" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/services/{service}:
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Services]
|
|
summary: Show a service
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
responses:
|
|
"200":
|
|
description: The service
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Service" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { description: Not found }
|
|
patch:
|
|
tags: [Services]
|
|
summary: Update a service
|
|
description: Partial update. Translatable fields merge into the existing map.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ServiceWrite" }
|
|
responses:
|
|
"200":
|
|
description: Service updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Service" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
delete:
|
|
tags: [Services]
|
|
summary: Soft-delete a service
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/services/{service}/restore:
|
|
post:
|
|
tags: [Services]
|
|
summary: Restore a soft-deleted service
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: Service restored
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Service" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/clients:
|
|
get:
|
|
tags: [Clients]
|
|
summary: List clients
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: is_public
|
|
in: query
|
|
schema: { type: boolean }
|
|
- name: search
|
|
in: query
|
|
description: Matches the name.
|
|
schema: { type: string }
|
|
- name: trashed
|
|
in: query
|
|
schema: { type: boolean }
|
|
responses:
|
|
"200":
|
|
description: Paginated clients
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ClientCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
post:
|
|
tags: [Clients]
|
|
summary: Create a client
|
|
description: |
|
|
Send the `path` from `POST /v1/media` with `for=clients` as `logo`.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ClientWrite" }
|
|
responses:
|
|
"201":
|
|
description: Client created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Client" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/clients/{client}:
|
|
parameters:
|
|
- name: client
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Clients]
|
|
summary: Show a client
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
responses:
|
|
"200":
|
|
description: The client
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Client" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { description: Not found }
|
|
patch:
|
|
tags: [Clients]
|
|
summary: Update a client
|
|
description: Partial update. Translatable fields merge into the existing map.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ClientWrite" }
|
|
responses:
|
|
"200":
|
|
description: Client updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Client" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
delete:
|
|
tags: [Clients]
|
|
summary: Soft-delete a client
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/clients/{client}/restore:
|
|
post:
|
|
tags: [Clients]
|
|
summary: Restore a soft-deleted client
|
|
parameters:
|
|
- name: client
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: Client restored
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Client" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/testimonials:
|
|
get:
|
|
tags: [Testimonials]
|
|
summary: List testimonials
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: is_public
|
|
in: query
|
|
schema: { type: boolean }
|
|
- name: client_id
|
|
in: query
|
|
schema: { type: integer }
|
|
- name: project_id
|
|
in: query
|
|
schema: { type: integer }
|
|
- name: service_id
|
|
in: query
|
|
schema: { type: integer }
|
|
- name: search
|
|
in: query
|
|
description: Matches the author's name.
|
|
schema: { type: string }
|
|
- name: trashed
|
|
in: query
|
|
schema: { type: boolean }
|
|
responses:
|
|
"200":
|
|
description: Paginated testimonials
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/TestimonialCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
post:
|
|
tags: [Testimonials]
|
|
summary: Create a testimonial
|
|
description: |
|
|
`quote` holds what the person said in `original_locale`, and any
|
|
translations of it. Send the `path` from `POST /v1/media` with
|
|
`for=testimonials` as `avatar`.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/TestimonialWrite" }
|
|
responses:
|
|
"201":
|
|
description: Testimonial created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Testimonial" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/testimonials/{testimonial}:
|
|
parameters:
|
|
- name: testimonial
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Testimonials]
|
|
summary: Show a testimonial
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
responses:
|
|
"200":
|
|
description: The testimonial
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Testimonial" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { description: Not found }
|
|
patch:
|
|
tags: [Testimonials]
|
|
summary: Update a testimonial
|
|
description: Partial update. Translatable fields merge into the existing map.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/TestimonialWrite" }
|
|
responses:
|
|
"200":
|
|
description: Testimonial updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Testimonial" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
delete:
|
|
tags: [Testimonials]
|
|
summary: Soft-delete a testimonial
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/testimonials/{testimonial}/restore:
|
|
post:
|
|
tags: [Testimonials]
|
|
summary: Restore a soft-deleted testimonial
|
|
parameters:
|
|
- name: testimonial
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: Testimonial restored
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Testimonial" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/talks:
|
|
get:
|
|
tags: [Talks]
|
|
summary: List talks
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: is_public
|
|
in: query
|
|
schema: { type: boolean }
|
|
- name: member_id
|
|
in: query
|
|
schema: { type: integer }
|
|
- name: project_id
|
|
in: query
|
|
schema: { type: integer }
|
|
- name: search
|
|
in: query
|
|
description: Matches the title or the event.
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated talks
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/TalkCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
post:
|
|
tags: [Talks]
|
|
summary: Create a talk
|
|
description: |
|
|
`title` is the one the talk was given under, in `language`. Only
|
|
`summary` is translated.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/TalkWrite" }
|
|
responses:
|
|
"201":
|
|
description: Talk created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Talk" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/talks/{talk}:
|
|
parameters:
|
|
- name: talk
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Talks]
|
|
summary: Show a talk
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
responses:
|
|
"200":
|
|
description: The talk
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Talk" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { description: Not found }
|
|
patch:
|
|
tags: [Talks]
|
|
summary: Update a talk
|
|
description: Partial update. Translatable fields merge into the existing map.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/TalkWrite" }
|
|
responses:
|
|
"200":
|
|
description: Talk updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Talk" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
delete:
|
|
tags: [Talks]
|
|
summary: Delete a talk
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/calls:
|
|
get:
|
|
tags: [Calls]
|
|
summary: List calls
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: status
|
|
in: query
|
|
description: "`open`, `filled` or `closed`."
|
|
schema: { type: string }
|
|
- name: project_id
|
|
in: query
|
|
schema: { type: integer }
|
|
- name: search
|
|
in: query
|
|
description: Matches the title within the active language only.
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated calls
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/CallCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
post:
|
|
tags: [Calls]
|
|
summary: Create a call
|
|
description: |
|
|
`technology_ids` is replaced whole when present.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/CallWrite" }
|
|
responses:
|
|
"201":
|
|
description: Call created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Call" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/calls/{call}:
|
|
parameters:
|
|
- name: call
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Calls]
|
|
summary: Show a call
|
|
parameters:
|
|
- $ref: "#/components/parameters/Lang"
|
|
- $ref: "#/components/parameters/With"
|
|
responses:
|
|
"200":
|
|
description: The call
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Call" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { description: Not found }
|
|
patch:
|
|
tags: [Calls]
|
|
summary: Update a call
|
|
description: Partial update. Translatable fields merge into the existing map.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/CallWrite" }
|
|
responses:
|
|
"200":
|
|
description: Call updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Call" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
delete:
|
|
tags: [Calls]
|
|
summary: Delete a call
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/applications:
|
|
get:
|
|
tags: [Applications]
|
|
summary: List applications
|
|
parameters:
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: status
|
|
in: query
|
|
description: "`new`, `contacted`, `accepted`, `declined` or `spam`. Without it, spam is left out."
|
|
schema: { type: string }
|
|
- name: kind
|
|
in: query
|
|
description: "`contribute` or `request`."
|
|
schema: { type: string }
|
|
- name: call_id
|
|
in: query
|
|
schema: { type: integer }
|
|
- name: search
|
|
in: query
|
|
description: Matches the name, e-mail or organisation.
|
|
schema: { type: string }
|
|
- name: trashed
|
|
in: query
|
|
schema: { type: boolean }
|
|
responses:
|
|
"200":
|
|
description: Paginated applications
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ApplicationCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/applications/{application}:
|
|
parameters:
|
|
- name: application
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Applications]
|
|
summary: Show an application
|
|
responses:
|
|
"200":
|
|
description: The application
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Application" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { description: Not found }
|
|
patch:
|
|
tags: [Applications]
|
|
summary: Handle an application
|
|
description: |
|
|
Only how it is being handled changes: `status` and the private
|
|
`notes`. Moving it to `contacted`, `accepted` or `declined` records who
|
|
did it and when; back to `new` clears both.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ApplicationWrite" }
|
|
responses:
|
|
"200":
|
|
description: Application updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Application" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
delete:
|
|
tags: [Applications]
|
|
summary: Soft-delete an application
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/applications/{application}/restore:
|
|
post:
|
|
tags: [Applications]
|
|
summary: Restore a soft-deleted application
|
|
parameters:
|
|
- name: application
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: Application restored
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Application" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/redirects:
|
|
get:
|
|
tags: [Redirects]
|
|
summary: List redirects
|
|
parameters:
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: is_enabled
|
|
in: query
|
|
schema: { type: boolean }
|
|
- name: locale
|
|
in: query
|
|
schema: { type: string }
|
|
- name: automatic
|
|
in: query
|
|
description: "True for the ones a slug rename left behind, false for those typed by hand."
|
|
schema: { type: boolean }
|
|
- name: search
|
|
in: query
|
|
description: "Matches either end."
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated redirects
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RedirectCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
post:
|
|
tags: [Redirects]
|
|
summary: Create a redirect
|
|
description: |
|
|
Both ends are normalised — leading slash, no trailing one, no query
|
|
string. A `locale` of null applies in every language. Creating one marks
|
|
the matching missing URLs as resolved.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RedirectWrite" }
|
|
responses:
|
|
"201":
|
|
description: Redirect created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Redirect" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/redirects/{redirect}:
|
|
parameters:
|
|
- name: redirect
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Redirects]
|
|
summary: Show a redirect
|
|
responses:
|
|
"200":
|
|
description: The redirect
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Redirect" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { description: Not found }
|
|
patch:
|
|
tags: [Redirects]
|
|
summary: Update a redirect
|
|
description: Partial update.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RedirectWrite" }
|
|
responses:
|
|
"200":
|
|
description: Redirect updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Redirect" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
delete:
|
|
tags: [Redirects]
|
|
summary: Delete a redirect
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/missing-urls:
|
|
get:
|
|
tags: [Redirects]
|
|
summary: List missing urls
|
|
parameters:
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: status
|
|
in: query
|
|
description: "`pending` (the default), `resolved` or `ignored`."
|
|
schema: { type: string }
|
|
- name: locale
|
|
in: query
|
|
schema: { type: string }
|
|
- name: search
|
|
in: query
|
|
description: "Matches the path."
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated missing URLs
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/MissingUrlCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/missing-urls/{missing_url}:
|
|
parameters:
|
|
- name: missing_url
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Redirects]
|
|
summary: Show a missing URL
|
|
responses:
|
|
"200":
|
|
description: The missing URL
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/MissingUrl" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { description: Not found }
|
|
patch:
|
|
tags: [Redirects]
|
|
summary: Ignore a missing URL, or stop ignoring it
|
|
description: "`status` moves between `pending` and `ignored` only: `resolved` is what giving it a redirect does."
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/MissingUrlWrite" }
|
|
responses:
|
|
"200":
|
|
description: MissingUrl updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/MissingUrl" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
delete:
|
|
tags: [Redirects]
|
|
summary: Delete a missing URL
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/missing-urls/{missing_url}/redirect:
|
|
post:
|
|
tags: [Redirects]
|
|
summary: Give a missing URL a destination
|
|
description: |
|
|
Creates the redirect from its path, which marks it resolved. `locale`
|
|
defaults to the language it was seen in; send null for every language.
|
|
parameters:
|
|
- name: missing_url
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [to]
|
|
properties:
|
|
to: { type: string, maxLength: 500 }
|
|
status: { type: integer, enum: [301, 302], default: 301 }
|
|
locale: { type: string, nullable: true }
|
|
responses:
|
|
"201":
|
|
description: Redirect created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Redirect" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/repositories:
|
|
get:
|
|
tags: [Repositories]
|
|
summary: List repositories
|
|
parameters:
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: provider
|
|
in: query
|
|
description: "`gitea`, `github` or `gitlab`."
|
|
schema: { type: string }
|
|
- name: project_id
|
|
in: query
|
|
schema: { type: integer }
|
|
- name: show_publicly
|
|
in: query
|
|
schema: { type: boolean }
|
|
- name: failing
|
|
in: query
|
|
description: "Only those whose sync has failed three times or more."
|
|
schema: { type: boolean }
|
|
- name: search
|
|
in: query
|
|
description: "Matches the full name."
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated repositories
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RepositoryCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
post:
|
|
tags: [Repositories]
|
|
summary: Create a repository
|
|
description: |
|
|
Only which repository, whose project and whether to show it. Everything
|
|
else is what the provider reports on the next sync.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RepositoryWrite" }
|
|
responses:
|
|
"201":
|
|
description: Repository created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Repository" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/repositories/{repository}:
|
|
parameters:
|
|
- name: repository
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Repositories]
|
|
summary: Show a repository
|
|
responses:
|
|
"200":
|
|
description: The repository
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Repository" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { description: Not found }
|
|
patch:
|
|
tags: [Repositories]
|
|
summary: Update a repository
|
|
description: Partial update.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RepositoryWrite" }
|
|
responses:
|
|
"200":
|
|
description: Repository updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Repository" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
delete:
|
|
tags: [Repositories]
|
|
summary: Delete a repository
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/repositories/{repository}/sync:
|
|
post:
|
|
tags: [Repositories]
|
|
summary: Sync a repository
|
|
description: |
|
|
Queues a sync with the provider and answers straight away. Requires
|
|
`repositories.sync`: a sync spends a rate limit the whole application
|
|
shares.
|
|
parameters:
|
|
- name: repository
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"202":
|
|
description: Sync queued
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Repository" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/settings:
|
|
get:
|
|
tags: [Settings]
|
|
summary: List settings
|
|
description: Every stored setting, unpaginated. Requires `settings.view`.
|
|
responses:
|
|
"200":
|
|
description: The settings
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Setting" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/settings/{setting}:
|
|
parameters:
|
|
- name: setting
|
|
in: path
|
|
required: true
|
|
description: "The setting's key, e.g. `site.maintenance`."
|
|
schema: { type: string }
|
|
get:
|
|
tags: [Settings]
|
|
summary: Show a setting
|
|
responses:
|
|
"200":
|
|
description: The setting
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Setting" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { description: Not found }
|
|
patch:
|
|
tags: [Settings]
|
|
summary: Update a setting
|
|
description: |
|
|
Replaces the value whole. Requires `settings.update`, or
|
|
`settings.maintenance` for `site.maintenance`. `content.version` is
|
|
written by the application and refused here.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [value]
|
|
properties:
|
|
value: { description: "Any JSON value, null included." }
|
|
responses:
|
|
"200":
|
|
description: Setting updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Setting" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/maintenance:
|
|
get:
|
|
tags: [Settings]
|
|
summary: Show maintenance mode
|
|
description: The public site's maintenance mode. Requires `settings.view`.
|
|
responses:
|
|
"200":
|
|
description: Maintenance state
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Maintenance" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
put:
|
|
tags: [Settings]
|
|
summary: Switch maintenance mode
|
|
description: |
|
|
Only the public site goes down; the panel and this API keep working.
|
|
`message` (language → text) and `until` are kept either way, so the
|
|
wording can be written before switching it on. A passed `until` lifts
|
|
it by itself. Requires `settings.maintenance`.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [enabled]
|
|
properties:
|
|
enabled: { type: boolean }
|
|
message: { type: object, nullable: true, additionalProperties: { type: string, maxLength: 500 } }
|
|
until: { type: string, format: date-time, nullable: true }
|
|
responses:
|
|
"200":
|
|
description: Maintenance state
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Maintenance" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/pages/{page}/revisions:
|
|
get:
|
|
tags: [History]
|
|
summary: List the history of a page
|
|
description: Who changed what and when, newest first. Binned records included.
|
|
parameters:
|
|
- name: page
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: event
|
|
in: query
|
|
description: "`created`, `updated`, `published`, `scheduled`, `unpublished`, `deleted`, `restored` or `erased`."
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated revisions
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RevisionCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/categories/{category}/revisions:
|
|
get:
|
|
tags: [History]
|
|
summary: List the history of a category
|
|
description: Who changed what and when, newest first. Binned records included.
|
|
parameters:
|
|
- name: category
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: event
|
|
in: query
|
|
description: "`created`, `updated`, `published`, `scheduled`, `unpublished`, `deleted`, `restored` or `erased`."
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated revisions
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RevisionCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/projects/{project}/revisions:
|
|
get:
|
|
tags: [History]
|
|
summary: List the history of a project
|
|
description: Who changed what and when, newest first. Binned records included.
|
|
parameters:
|
|
- name: project
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: event
|
|
in: query
|
|
description: "`created`, `updated`, `published`, `scheduled`, `unpublished`, `deleted`, `restored` or `erased`."
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated revisions
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RevisionCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/members/{member}/revisions:
|
|
get:
|
|
tags: [History]
|
|
summary: List the history of a member
|
|
description: Who changed what and when, newest first. Binned records included.
|
|
parameters:
|
|
- name: member
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: event
|
|
in: query
|
|
description: "`created`, `updated`, `published`, `scheduled`, `unpublished`, `deleted`, `restored` or `erased`."
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated revisions
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RevisionCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/services/{service}/revisions:
|
|
get:
|
|
tags: [History]
|
|
summary: List the history of a service
|
|
description: Who changed what and when, newest first. Binned records included.
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: event
|
|
in: query
|
|
description: "`created`, `updated`, `published`, `scheduled`, `unpublished`, `deleted`, `restored` or `erased`."
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated revisions
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RevisionCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/clients/{client}/revisions:
|
|
get:
|
|
tags: [History]
|
|
summary: List the history of a client
|
|
description: Who changed what and when, newest first. Binned records included.
|
|
parameters:
|
|
- name: client
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: event
|
|
in: query
|
|
description: "`created`, `updated`, `published`, `scheduled`, `unpublished`, `deleted`, `restored` or `erased`."
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated revisions
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RevisionCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/testimonials/{testimonial}/revisions:
|
|
get:
|
|
tags: [History]
|
|
summary: List the history of a testimonial
|
|
description: Who changed what and when, newest first. Binned records included.
|
|
parameters:
|
|
- name: testimonial
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: event
|
|
in: query
|
|
description: "`created`, `updated`, `published`, `scheduled`, `unpublished`, `deleted`, `restored` or `erased`."
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated revisions
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RevisionCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/talks/{talk}/revisions:
|
|
get:
|
|
tags: [History]
|
|
summary: List the history of a talk
|
|
description: Who changed what and when, newest first. Binned records included.
|
|
parameters:
|
|
- name: talk
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: event
|
|
in: query
|
|
description: "`created`, `updated`, `published`, `scheduled`, `unpublished`, `deleted`, `restored` or `erased`."
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated revisions
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RevisionCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/technologies/{technology}/revisions:
|
|
get:
|
|
tags: [History]
|
|
summary: List the history of a technology
|
|
description: Who changed what and when, newest first. Binned records included.
|
|
parameters:
|
|
- name: technology
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: event
|
|
in: query
|
|
description: "`created`, `updated`, `published`, `scheduled`, `unpublished`, `deleted`, `restored` or `erased`."
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated revisions
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RevisionCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/calls/{call}/revisions:
|
|
get:
|
|
tags: [History]
|
|
summary: List the history of a call
|
|
description: Who changed what and when, newest first. Binned records included.
|
|
parameters:
|
|
- name: call
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: event
|
|
in: query
|
|
description: "`created`, `updated`, `published`, `scheduled`, `unpublished`, `deleted`, `restored` or `erased`."
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated revisions
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RevisionCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/revisions/{revision}/restore:
|
|
post:
|
|
tags: [History]
|
|
summary: Undo a revision
|
|
description: |
|
|
Puts back the values one revision changed, as the panel's history
|
|
does. Restoring is itself a change and lands in the history. Requires
|
|
permission to edit the record, and to publish it when the revision
|
|
touched its publication.
|
|
parameters:
|
|
- name: revision
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: The revision the restore recorded
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Revision" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/reviews:
|
|
get:
|
|
tags: [Reviews]
|
|
summary: What is waiting for my review
|
|
description: |
|
|
Pages, projects and services awaiting review that the caller could
|
|
publish — addressed to them, or to nobody in particular. Oldest
|
|
request first, unpaginated.
|
|
responses:
|
|
"200":
|
|
description: The queue
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/ReviewState" }
|
|
|
|
/v1/pages/{page}/reviews:
|
|
get:
|
|
tags: [Reviews]
|
|
summary: Review thread of a page
|
|
description: Requests, returns, publications and comments, newest first.
|
|
parameters:
|
|
- name: page
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
- $ref: "#/components/parameters/PerPage"
|
|
responses:
|
|
"200":
|
|
description: Paginated thread
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ReviewCommentCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/pages/{page}/comments:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Comment on the review of a page
|
|
parameters:
|
|
- name: page
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [body]
|
|
properties:
|
|
body: { type: string, maxLength: 2000 }
|
|
responses:
|
|
"201":
|
|
description: Comment added
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/ReviewComment" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/pages/{page}/review:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Ask for a page to be reviewed
|
|
description: |
|
|
The way to put it live without `pages.publish`: whoever can publish it
|
|
is told. `reviewer_id` addresses one of them; without it, anyone may
|
|
answer. Refused if it is already waiting or already published.
|
|
parameters:
|
|
- name: page
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
reviewer_id: { type: integer, nullable: true }
|
|
note: { type: string, nullable: true, maxLength: 2000 }
|
|
responses:
|
|
"200":
|
|
description: Where the review stands
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/ReviewState" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/pages/{page}/return:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Send a page back for changes
|
|
description: Tells whoever asked, with the reason. Requires `pages.publish`.
|
|
parameters:
|
|
- name: page
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [note]
|
|
properties:
|
|
note: { type: string, maxLength: 2000 }
|
|
responses:
|
|
"200":
|
|
description: Where the review stands
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/ReviewState" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/pages/{page}/publish:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Publish a page, now or later
|
|
description: |
|
|
Approves it: live now, or from `at` when that is in the future. Any
|
|
pending review is answered either way. Requires `pages.publish`.
|
|
parameters:
|
|
- name: page
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
at: { type: string, format: date-time, nullable: true }
|
|
responses:
|
|
"200":
|
|
description: Where the review stands
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/ReviewState" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/pages/{page}/unpublish:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Take a page off the site
|
|
description: Requires `pages.publish`.
|
|
parameters:
|
|
- name: page
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: Where the review stands
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/ReviewState" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/projects/{project}/reviews:
|
|
get:
|
|
tags: [Reviews]
|
|
summary: Review thread of a project
|
|
description: Requests, returns, publications and comments, newest first.
|
|
parameters:
|
|
- name: project
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
- $ref: "#/components/parameters/PerPage"
|
|
responses:
|
|
"200":
|
|
description: Paginated thread
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ReviewCommentCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/projects/{project}/comments:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Comment on the review of a project
|
|
parameters:
|
|
- name: project
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [body]
|
|
properties:
|
|
body: { type: string, maxLength: 2000 }
|
|
responses:
|
|
"201":
|
|
description: Comment added
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/ReviewComment" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/projects/{project}/review:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Ask for a project to be reviewed
|
|
description: |
|
|
The way to put it live without `projects.publish`: whoever can publish it
|
|
is told. `reviewer_id` addresses one of them; without it, anyone may
|
|
answer. Refused if it is already waiting or already published.
|
|
parameters:
|
|
- name: project
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
reviewer_id: { type: integer, nullable: true }
|
|
note: { type: string, nullable: true, maxLength: 2000 }
|
|
responses:
|
|
"200":
|
|
description: Where the review stands
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/ReviewState" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/projects/{project}/return:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Send a project back for changes
|
|
description: Tells whoever asked, with the reason. Requires `projects.publish`.
|
|
parameters:
|
|
- name: project
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [note]
|
|
properties:
|
|
note: { type: string, maxLength: 2000 }
|
|
responses:
|
|
"200":
|
|
description: Where the review stands
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/ReviewState" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/projects/{project}/publish:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Publish a project, now or later
|
|
description: |
|
|
Approves it: live now, or from `at` when that is in the future. Any
|
|
pending review is answered either way. Requires `projects.publish`.
|
|
parameters:
|
|
- name: project
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
at: { type: string, format: date-time, nullable: true }
|
|
responses:
|
|
"200":
|
|
description: Where the review stands
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/ReviewState" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/projects/{project}/unpublish:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Take a project off the site
|
|
description: Requires `projects.publish`.
|
|
parameters:
|
|
- name: project
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: Where the review stands
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/ReviewState" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/services/{service}/reviews:
|
|
get:
|
|
tags: [Reviews]
|
|
summary: Review thread of a service
|
|
description: Requests, returns, publications and comments, newest first.
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
- $ref: "#/components/parameters/PerPage"
|
|
responses:
|
|
"200":
|
|
description: Paginated thread
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ReviewCommentCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/services/{service}/comments:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Comment on the review of a service
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [body]
|
|
properties:
|
|
body: { type: string, maxLength: 2000 }
|
|
responses:
|
|
"201":
|
|
description: Comment added
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/ReviewComment" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/services/{service}/review:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Ask for a service to be reviewed
|
|
description: |
|
|
The way to put it live without `services.publish`: whoever can publish it
|
|
is told. `reviewer_id` addresses one of them; without it, anyone may
|
|
answer. Refused if it is already waiting or already published.
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
reviewer_id: { type: integer, nullable: true }
|
|
note: { type: string, nullable: true, maxLength: 2000 }
|
|
responses:
|
|
"200":
|
|
description: Where the review stands
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/ReviewState" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/services/{service}/return:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Send a service back for changes
|
|
description: Tells whoever asked, with the reason. Requires `services.publish`.
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [note]
|
|
properties:
|
|
note: { type: string, maxLength: 2000 }
|
|
responses:
|
|
"200":
|
|
description: Where the review stands
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/ReviewState" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/services/{service}/publish:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Publish a service, now or later
|
|
description: |
|
|
Approves it: live now, or from `at` when that is in the future. Any
|
|
pending review is answered either way. Requires `services.publish`.
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
at: { type: string, format: date-time, nullable: true }
|
|
responses:
|
|
"200":
|
|
description: Where the review stands
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/ReviewState" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/services/{service}/unpublish:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Take a service off the site
|
|
description: Requires `services.publish`.
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: Where the review stands
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/ReviewState" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/pages/{page}/preview:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Preview links for a page
|
|
description: |
|
|
One link per language it has an address in, to see it on the real
|
|
site before it is published. Each opens this record only and expires
|
|
a few hours later.
|
|
parameters:
|
|
- name: page
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"201":
|
|
description: Preview links
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/PreviewLinks" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/projects/{project}/preview:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Preview links for a project
|
|
description: |
|
|
One link per language it has an address in, to see it on the real
|
|
site before it is published. Each opens this record only and expires
|
|
a few hours later.
|
|
parameters:
|
|
- name: project
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"201":
|
|
description: Preview links
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/PreviewLinks" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/services/{service}/preview:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Preview links for a service
|
|
description: |
|
|
One link per language it has an address in, to see it on the real
|
|
site before it is published. Each opens this record only and expires
|
|
a few hours later.
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"201":
|
|
description: Preview links
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/PreviewLinks" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/calls/{call}/preview:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Preview links for a call
|
|
description: |
|
|
One link per language it has an address in, to see it on the real
|
|
site before it is published. Each opens this record only and expires
|
|
a few hours later.
|
|
parameters:
|
|
- name: call
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"201":
|
|
description: Preview links
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/PreviewLinks" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/health:
|
|
get:
|
|
tags: [Health]
|
|
summary: Site health
|
|
description: |
|
|
The panel's health screen: every check the caller may see, problems
|
|
first — broken images, missing slugs, stale translations, unanswered
|
|
submissions… A check behind a permission the caller lacks is left out.
|
|
responses:
|
|
"200":
|
|
description: The findings
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/HealthFinding" }
|
|
meta:
|
|
type: object
|
|
properties:
|
|
problems: { type: integer }
|
|
checked_at: { type: string, format: date-time }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/translations/pending:
|
|
get:
|
|
tags: [Health]
|
|
summary: What is left to translate
|
|
description: |
|
|
Pages, projects, services and calls with a language still to write
|
|
(`missing`) or left behind by the one it was translated from (`stale`,
|
|
with since when). Unpaginated.
|
|
parameters:
|
|
- name: locale
|
|
in: query
|
|
description: "Only what this language lacks."
|
|
schema: { type: string }
|
|
- name: type
|
|
in: query
|
|
description: "`pages`, `projects`, `services` or `calls`."
|
|
schema: { type: string }
|
|
- name: published
|
|
in: query
|
|
description: "Only what is on the site \u2014 open, for calls."
|
|
schema: { type: boolean }
|
|
responses:
|
|
"200":
|
|
description: The records
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PendingTranslation" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/batch:
|
|
post:
|
|
tags: [Batch]
|
|
summary: Run several operations
|
|
description: |
|
|
Up to 50 admin API calls in one request. Each runs as a request of its
|
|
own with the caller's credentials — validated, authorised and answered
|
|
exactly as if sent separately — so a batch can do nothing its
|
|
operations would be refused. Their answers come back in order.
|
|
|
|
With `atomic`, it stops at the first failure and undoes everything
|
|
before it. What already left the database — a queued job, a
|
|
notification — is not undone.
|
|
|
|
`path` is an admin path such as `/v1/pages/3`; batches do not nest and
|
|
do not reach the import or the token exchange.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [operations]
|
|
properties:
|
|
atomic: { type: boolean, default: false }
|
|
operations:
|
|
type: array
|
|
maxItems: 50
|
|
items:
|
|
type: object
|
|
required: [method, path]
|
|
properties:
|
|
method: { type: string, enum: [GET, POST, PUT, PATCH, DELETE] }
|
|
path: { type: string, example: /v1/technologies }
|
|
body: { type: object, nullable: true, additionalProperties: true }
|
|
responses:
|
|
"200":
|
|
description: Each operation's answer
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/BatchOutcome" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/import/{resource}:
|
|
post:
|
|
tags: [Batch]
|
|
summary: Import records
|
|
description: |
|
|
Rows of one kind of record, as JSON (`rows`) or a CSV `file`, turned
|
|
into the create and update calls the API already has — up to 50 per
|
|
import, each validated and authorised like any other call.
|
|
|
|
A row with an `id`, or whose `match` field (`slug`, `name` or
|
|
`full_name`) names an existing record, updates it; any other row
|
|
creates one. In a CSV, a column `title.es` fills the Spanish title, a
|
|
list is written `1|2|3`, `true`/`false`/`null` are what they say, and
|
|
an empty cell is left out rather than erasing what is there.
|
|
|
|
All-or-nothing unless `atomic` is false. `dry_run` checks every row and
|
|
keeps none.
|
|
parameters:
|
|
- name: resource
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum: [pages, categories, projects, members, services, clients, testimonials, talks, technologies, calls]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [rows]
|
|
properties:
|
|
rows:
|
|
type: array
|
|
maxItems: 50
|
|
items: { type: object, additionalProperties: true, description: The same body the resource's create endpoint takes. }
|
|
match: { type: string, enum: [slug, name, full_name], nullable: true }
|
|
atomic: { type: boolean, default: true }
|
|
dry_run: { type: boolean, default: false }
|
|
multipart/form-data:
|
|
schema:
|
|
type: object
|
|
required: [file]
|
|
properties:
|
|
file: { type: string, format: binary, description: "CSV with a header row, UTF-8." }
|
|
match: { type: string, enum: [slug, name, full_name] }
|
|
atomic: { type: boolean, default: true }
|
|
dry_run: { type: boolean, default: false }
|
|
responses:
|
|
"200":
|
|
description: Each row's answer
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/BatchOutcome" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/webhooks:
|
|
get:
|
|
tags: [Webhooks]
|
|
summary: List webhooks
|
|
parameters:
|
|
- $ref: "#/components/parameters/PerPage"
|
|
responses:
|
|
"200":
|
|
description: Paginated webhooks
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/WebhookCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
post:
|
|
tags: [Webhooks]
|
|
summary: Create a webhook
|
|
description: |
|
|
Somewhere to POST to when something happens here. HTTPS to a public
|
|
address only — checked now and again at every delivery — and redirects are
|
|
not followed. The `secret` signing the deliveries is generated unless sent,
|
|
and is in this answer's `secret` once, never again.
|
|
|
|
Each delivery is a JSON body `{id, event, created_at, data}` with headers
|
|
`X-Webhook-Event`, `X-Webhook-Delivery`, `X-Webhook-Timestamp` and
|
|
`X-Webhook-Signature`: `sha256=` and the HMAC-SHA256 of
|
|
`{timestamp}.{body}` with the secret. Failed deliveries are retried five
|
|
times over about two and a half hours; twenty failures in a row switch the
|
|
webhook off.
|
|
|
|
Events with a meaning of their own: `application.received` (its
|
|
sender's details only with `include_personal_data`),
|
|
`content.published`, `content.unpublished`, `review.requested`,
|
|
`review.returned`.
|
|
|
|
And every change a person makes to a record, as `{resource}.{action}`
|
|
— `projects.created`, `pages.updated`, `media.deleted`,
|
|
`calls.restored` — for pages, categories, tags, projects, members,
|
|
services, clients, testimonials, talks, technologies, calls,
|
|
redirects, repositories, media and applications. Its `data` says
|
|
what (`type`, `id`, `title`, `url` when it is on the site), who
|
|
(`by`), how they came in (`via`: panel, api, token) and, on an
|
|
update, `changes` as field → `{old, new}`. What the application does
|
|
by itself — a sync, a scheduled publication — is not sent. Changes to
|
|
an application carry only how it is handled unless
|
|
`include_personal_data`. Users, tokens, webhooks and settings are
|
|
never sent.
|
|
|
|
Subscribe to an event, to `*`, to all of one kind of record
|
|
(`projects.*`) or to one action on any (`*.deleted`).
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/WebhookWrite" }
|
|
responses:
|
|
"201":
|
|
description: Webhook created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Webhook" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/webhooks/{webhook}:
|
|
parameters:
|
|
- name: webhook
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Webhooks]
|
|
summary: Show a webhook
|
|
responses:
|
|
"200":
|
|
description: The webhook
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Webhook" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { description: Not found }
|
|
patch:
|
|
tags: [Webhooks]
|
|
summary: Update a webhook
|
|
description: "Partial update. `rotate_secret` makes a new secret, in this answer's `secret` once. Switching it back on resets its failure count."
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/WebhookWrite" }
|
|
responses:
|
|
"200":
|
|
description: Webhook updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Webhook" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
delete:
|
|
tags: [Webhooks]
|
|
summary: Delete a webhook
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/webhooks/{webhook}/deliveries:
|
|
get:
|
|
tags: [Webhooks]
|
|
summary: Deliveries of a webhook
|
|
description: Newest first.
|
|
parameters:
|
|
- name: webhook
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: status
|
|
in: query
|
|
description: "`pending`, `succeeded` or `failed`."
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated deliveries
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/WebhookDeliveryCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/webhooks/{webhook}/ping:
|
|
post:
|
|
tags: [Webhooks]
|
|
summary: Send a test delivery
|
|
description: A `ping` event, to check the receiving end.
|
|
parameters:
|
|
- name: webhook
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"202":
|
|
description: Ping queued
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/WebhookDelivery" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/webhook-deliveries/{delivery}/redeliver:
|
|
post:
|
|
tags: [Webhooks]
|
|
summary: Send a delivery again
|
|
description: As a new delivery of the same event and payload.
|
|
parameters:
|
|
- name: delivery
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"202":
|
|
description: Delivery queued
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/WebhookDelivery" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/audit:
|
|
get:
|
|
tags: [Audit]
|
|
summary: The audit log
|
|
description: |
|
|
Who did what, when, and how they came in — the panel, the API, a
|
|
machine token. Every change a person makes to content, the inbox,
|
|
redirects, repositories, media, settings, users, tokens and webhooks;
|
|
role changes; signing in to the panel. What the application does by
|
|
itself (a sync, a scheduled publication) is not here; each record's
|
|
own history has it. Secrets are never written down. Kept for a year
|
|
by default. Requires `audit.view_any`.
|
|
parameters:
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: user_id
|
|
in: query
|
|
schema: { type: integer }
|
|
- name: action
|
|
in: query
|
|
description: "Exact, e.g. `pages.updated`, or a prefix ending in a dot, e.g. `pages.`."
|
|
schema: { type: string }
|
|
- name: subject_type
|
|
in: query
|
|
description: "e.g. `pages`, `users`, `settings`."
|
|
schema: { type: string }
|
|
- name: subject_id
|
|
in: query
|
|
schema: { type: string }
|
|
- name: via
|
|
in: query
|
|
description: "`panel`, `api`, `token`, `web` or `console`."
|
|
schema: { type: string }
|
|
- name: from
|
|
in: query
|
|
schema: { type: string, format: date-time }
|
|
- name: to
|
|
in: query
|
|
schema: { type: string, format: date-time }
|
|
responses:
|
|
"200":
|
|
description: Paginated entries, newest first
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/AuditLogCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/pages/{page}/duplicate:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Duplicate a page as a draft
|
|
description: |
|
|
A copy to start the next one from: the same content and relations,
|
|
unpublished, out of any review, its title marked as a copy and a free
|
|
slug in each language.
|
|
parameters:
|
|
- name: page
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"201":
|
|
description: The copy
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: object
|
|
properties:
|
|
type: { type: string }
|
|
id: { type: integer }
|
|
title: { type: string }
|
|
slug: { type: object, additionalProperties: { type: string } }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/projects/{project}/duplicate:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Duplicate a project as a draft
|
|
description: |
|
|
A copy to start the next one from: the same content and relations,
|
|
unpublished, out of any review, its title marked as a copy and a free
|
|
slug in each language.
|
|
parameters:
|
|
- name: project
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"201":
|
|
description: The copy
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: object
|
|
properties:
|
|
type: { type: string }
|
|
id: { type: integer }
|
|
title: { type: string }
|
|
slug: { type: object, additionalProperties: { type: string } }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/services/{service}/duplicate:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Duplicate a service as a draft
|
|
description: |
|
|
A copy to start the next one from: the same content and relations,
|
|
unpublished, out of any review, its title marked as a copy and a free
|
|
slug in each language.
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"201":
|
|
description: The copy
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: object
|
|
properties:
|
|
type: { type: string }
|
|
id: { type: integer }
|
|
title: { type: string }
|
|
slug: { type: object, additionalProperties: { type: string } }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/calls/{call}/duplicate:
|
|
post:
|
|
tags: [Reviews]
|
|
summary: Duplicate a call as a draft
|
|
description: |
|
|
A copy to start the next one from: the same content and relations,
|
|
unpublished, out of any review, its title marked as a copy and a free
|
|
slug in each language.
|
|
parameters:
|
|
- name: call
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"201":
|
|
description: The copy
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: object
|
|
properties:
|
|
type: { type: string }
|
|
id: { type: integer }
|
|
title: { type: string }
|
|
slug: { type: object, additionalProperties: { type: string } }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/applications/{application}/replies:
|
|
get:
|
|
tags: [Applications]
|
|
summary: Replies given to an application
|
|
description: Newest first.
|
|
parameters:
|
|
- name: application
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: The replies
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/ApplicationReply" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/applications/{application}/reply:
|
|
post:
|
|
tags: [Applications]
|
|
summary: Reply to an application
|
|
description: |
|
|
`subject` and `body`, or a `template_id` to fill whichever is left out
|
|
— in the language the person wrote in. Sent by this application when
|
|
it has a mailer that delivers; otherwise recorded, and the answer
|
|
carries a `mailto` link to send it by hand. Moves a new application to
|
|
`contacted`.
|
|
parameters:
|
|
- name: application
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
template_id: { type: integer, nullable: true }
|
|
subject: { type: string, maxLength: 255 }
|
|
body: { type: string }
|
|
responses:
|
|
"201":
|
|
description: Reply recorded
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
allOf:
|
|
- $ref: "#/components/schemas/ApplicationReply"
|
|
- type: object
|
|
properties:
|
|
mailto: { type: string, nullable: true }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/reply-templates:
|
|
get:
|
|
tags: [Applications]
|
|
summary: Reply templates
|
|
description: Every template, in every language. Managed in the panel.
|
|
responses:
|
|
"200":
|
|
description: The templates
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/ReplyTemplate" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/media:
|
|
get:
|
|
tags: [Media]
|
|
summary: List the media library
|
|
description: Every image on the media disk, newest first. Requires `media.view_any`.
|
|
parameters:
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: for
|
|
in: query
|
|
description: The kind of record it was uploaded for.
|
|
schema: { $ref: "#/components/schemas/MediaFor" }
|
|
- name: mime
|
|
in: query
|
|
description: "e.g. `image/svg+xml`."
|
|
schema: { type: string }
|
|
- name: search
|
|
in: query
|
|
description: "Matches the path."
|
|
schema: { type: string }
|
|
- name: pending
|
|
in: query
|
|
description: "Only those the queue has not processed yet."
|
|
schema: { type: boolean }
|
|
- name: failed
|
|
in: query
|
|
description: "Only those whose processing failed."
|
|
schema: { type: boolean }
|
|
- name: orphans
|
|
in: query
|
|
description: "Only those nothing points at \u2014 what `content:prune --media` would delete."
|
|
schema: { type: boolean }
|
|
responses:
|
|
"200":
|
|
description: Paginated media
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/MediaCollection" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
post:
|
|
tags: [Media]
|
|
summary: Upload an image
|
|
description: |
|
|
Stores an image and answers with the `path` a record points at — send
|
|
it as a page's `featured_image`. Two ways in, exactly one per request:
|
|
|
|
- **`multipart/form-data`** with the image as `file`.
|
|
- **`application/json`** with a `url`, which the API downloads itself.
|
|
HTTPS only, from a public address — anything resolving to a private,
|
|
reserved or loopback address is refused — and redirects are not
|
|
followed: send the image's final address.
|
|
|
|
JPEG, PNG, WebP, GIF, AVIF or SVG, decided from the file's bytes rather
|
|
than its name or declared type. An SVG is stored sanitized: scripts,
|
|
event handlers and references to anything outside the file are
|
|
removed. 20 MB at most.
|
|
|
|
`for` says which kind of record the image is for — `pages` (the
|
|
default), `technologies`, `projects`, `members`, `clients` or
|
|
`testimonials` — which decides where it is stored and whose permission
|
|
it takes.
|
|
|
|
The smaller versions and the placeholder are derived afterwards, on the
|
|
queue: `processed` says whether that has happened, and the image serves
|
|
either way. Requires permission to create or edit the records named in
|
|
`for`.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
multipart/form-data:
|
|
schema:
|
|
type: object
|
|
required: [file]
|
|
properties:
|
|
file:
|
|
type: string
|
|
format: binary
|
|
description: The image.
|
|
for: { $ref: "#/components/schemas/MediaFor" }
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [url]
|
|
properties:
|
|
url:
|
|
type: string
|
|
format: uri
|
|
description: An `https://` address the API downloads the image from.
|
|
example: https://images.example.org/hero.jpg
|
|
for: { $ref: "#/components/schemas/MediaFor" }
|
|
responses:
|
|
"201":
|
|
description: Image stored
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Media" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/media/{media}:
|
|
parameters:
|
|
- name: media
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Media]
|
|
summary: Show an image
|
|
responses:
|
|
"200":
|
|
description: The image, with whether anything uses it
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Media" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { description: Not found }
|
|
delete:
|
|
tags: [Media]
|
|
summary: Delete an image
|
|
description: |
|
|
The file, its derived versions and its record. Only when nothing points
|
|
at it — a page, a logo, an image inside a text — since deleting one in
|
|
use would put a broken image on the public site. Requires
|
|
`media.delete`.
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"409": { description: The image is in use. }
|
|
|
|
/v1/media/{media}/reprocess:
|
|
post:
|
|
tags: [Media]
|
|
summary: Process an image again
|
|
description: Queues deriving the smaller versions and the placeholder again.
|
|
parameters:
|
|
- name: media
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"202":
|
|
description: Processing queued
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Media" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/users:
|
|
get:
|
|
tags: [Users]
|
|
summary: List local users
|
|
description: Requires `users.view_any`.
|
|
parameters:
|
|
- $ref: "#/components/parameters/PerPage"
|
|
- name: search
|
|
in: query
|
|
schema: { type: string }
|
|
- name: is_active
|
|
in: query
|
|
schema: { type: boolean }
|
|
- name: role
|
|
in: query
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: Paginated users
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/User" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/users/{user}:
|
|
parameters:
|
|
- name: user
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
get:
|
|
tags: [Users]
|
|
summary: Show a user
|
|
description: Reading your own record is always permitted.
|
|
responses:
|
|
"200":
|
|
description: The user
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/User" }
|
|
patch:
|
|
tags: [Users]
|
|
summary: Update a user's local access
|
|
description: |
|
|
Only fields this application owns are writable. Name and email belong
|
|
to the identity server and are refreshed on every sign-in, so change
|
|
those at `oauth.pireon.dev` instead.
|
|
|
|
Assigning roles additionally requires `roles.update`, and only a super
|
|
admin may grant `super_admin`.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
is_active: { type: boolean }
|
|
locale: { type: string, enum: [es, en, ca], nullable: true }
|
|
roles:
|
|
type: array
|
|
items: { type: string }
|
|
example: [editor]
|
|
responses:
|
|
"200":
|
|
description: User updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/User" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"422": { $ref: "#/components/responses/ValidationError" }
|
|
|
|
/v1/roles:
|
|
get:
|
|
tags: [Roles]
|
|
summary: List roles and their permissions
|
|
responses:
|
|
"200":
|
|
description: All roles
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Role" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
|
|
/v1/roles/catalogue:
|
|
get:
|
|
tags: [Roles]
|
|
summary: The full permission catalogue, grouped by resource
|
|
description: Useful for building a role editor outside the panel.
|
|
responses:
|
|
"200":
|
|
description: Permission catalogue
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: object
|
|
additionalProperties:
|
|
type: array
|
|
items: { type: string }
|
|
example:
|
|
general: [panel.access]
|
|
pages: [pages.view_any, pages.create, pages.publish]
|
|
|
|
/v1/roles/{role}:
|
|
get:
|
|
tags: [Roles]
|
|
summary: Show a role
|
|
parameters:
|
|
- name: role
|
|
in: path
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: The role
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data: { $ref: "#/components/schemas/Role" }
|
|
|
|
components:
|
|
securitySchemes:
|
|
bearerAuth:
|
|
type: http
|
|
scheme: bearer
|
|
description: |
|
|
An access token issued by https://oauth.pireon.dev, validated on each
|
|
request against the identity server's `GET /api/user` with the result
|
|
cached briefly — or a machine token (`pmt_…`) from `POST /v1/tokens`.
|
|
|
|
ingestToken:
|
|
type: apiKey
|
|
in: header
|
|
name: X-Ingest-Token
|
|
description: |
|
|
Shared secret held by the frontend, for the endpoints it calls on its
|
|
own behalf rather than on a visitor's. In a header rather than the
|
|
query string so it stays out of access logs. An unset secret disables
|
|
those endpoints instead of opening them.
|
|
|
|
parameters:
|
|
Lang:
|
|
name: lang
|
|
in: query
|
|
description: Language to resolve translatable fields into, overriding Accept-Language.
|
|
schema: { type: string, enum: [es, en, ca] }
|
|
With:
|
|
name: with
|
|
in: query
|
|
description: Comma-separated extras. `translations` adds every language.
|
|
schema: { type: string, example: translations }
|
|
PreviewToken:
|
|
name: X-Preview-Token
|
|
in: header
|
|
description: |
|
|
Shared secret that lets the frontend render unpublished content. When
|
|
valid, the `published` filter is skipped and the response is marked
|
|
`no-store`. An unset secret never grants access.
|
|
schema: { type: string }
|
|
IngestToken:
|
|
name: X-Ingest-Token
|
|
in: header
|
|
required: true
|
|
description: Shared secret for the endpoints the frontend calls on its own behalf.
|
|
schema: { type: string }
|
|
PerPage:
|
|
name: per_page
|
|
in: query
|
|
schema: { type: integer, default: 15, maximum: 100 }
|
|
|
|
responses:
|
|
Maintenance:
|
|
description: |
|
|
The public site is in maintenance. Returned by every `/public/v1`
|
|
endpoint while the flag is on, with the notice already resolved to the
|
|
request's language so the frontend can render a maintenance page from
|
|
it directly. Never cached, and accompanied by `Retry-After`.
|
|
|
|
The panel and the admin API are unaffected — this is deliberately not
|
|
Laravel's own maintenance mode.
|
|
headers:
|
|
Retry-After:
|
|
schema: { type: integer, example: 3600 }
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
message: { type: string, example: Estamos migrando el Gitea. Volvemos en un rato. }
|
|
maintenance: { type: boolean, example: true }
|
|
locale: { type: string, example: es }
|
|
since: { type: string, format: date-time, nullable: true }
|
|
until: { type: string, format: date-time, nullable: true }
|
|
Unauthorized:
|
|
description: Missing, expired, or revoked bearer token
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
message: { type: string, example: Unauthenticated. }
|
|
Forbidden:
|
|
description: The caller lacks the required local permission
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
message: { type: string, example: This action is unauthorized. }
|
|
ValidationError:
|
|
description: Validation failed
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
message: { type: string }
|
|
errors:
|
|
type: object
|
|
additionalProperties:
|
|
type: array
|
|
items: { type: string }
|
|
example:
|
|
slug: ['El slug «inicio» ya está en uso en el idioma «es».']
|
|
SubmissionAccepted:
|
|
description: |
|
|
Stored, and the people who can read the inbox have been told.
|
|
|
|
The body of the submission is never echoed back, so a guessed id
|
|
cannot be used to read somebody else's message. Always `no-store`.
|
|
headers:
|
|
Cache-Control:
|
|
schema: { type: string, example: "no-store, private" }
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/SubmissionReceipt" }
|
|
|
|
schemas:
|
|
|
|
Image:
|
|
type: object
|
|
description: |
|
|
Everything needed to render an image well server-side. A path alone
|
|
was never enough: without `width`/`height` the page reflows as images
|
|
arrive, without `srcset` every visitor downloads the original, and
|
|
without `alt` the page is not accessible.
|
|
|
|
Degrades on purpose — an upload whose derived versions are not ready
|
|
yet still carries a `url`, with the rest null, so the frontend renders
|
|
a plain `<img>` rather than a gap.
|
|
properties:
|
|
url:
|
|
type: string
|
|
format: uri
|
|
description: Absolute, because the frontend is on another host.
|
|
alt:
|
|
type: string
|
|
description: |
|
|
Empty string when the image is decorative — that is what `alt=""`
|
|
means — never absent. Follows the requested language.
|
|
width: { type: integer, nullable: true }
|
|
height: { type: integer, nullable: true }
|
|
aspect_ratio: { type: number, nullable: true }
|
|
mime: { type: string, nullable: true }
|
|
placeholder:
|
|
type: string
|
|
nullable: true
|
|
description: |
|
|
Tiny blurred data URI to paint while the real image loads.
|
|
srcset:
|
|
type: object
|
|
nullable: true
|
|
additionalProperties: { type: string }
|
|
description: |
|
|
Keyed by format, ready to use verbatim — build a `<picture>` with
|
|
AVIF first and WebP behind it. Only widths that actually exist are
|
|
listed, and an image is never scaled up.
|
|
example:
|
|
webp: "https://admin.example/storage/projects/cover-400.webp 400w, https://admin.example/storage/projects/cover-800.webp 800w"
|
|
|
|
ViewReport:
|
|
type: object
|
|
required: [type, slug]
|
|
properties:
|
|
type:
|
|
type: string
|
|
enum: [page, project, member, call, service]
|
|
description: |
|
|
The same vocabulary `/slugs` and the path configuration use, so a
|
|
report can only ever name something this API serves.
|
|
slug:
|
|
type: string
|
|
maxLength: 200
|
|
example: mi-web
|
|
description: The slug in the language given below, not a full path.
|
|
locale:
|
|
type: string
|
|
enum: [es, en, ca]
|
|
description: |
|
|
Which language was rendered. Falls back to the negotiated locale.
|
|
Counted separately per language, because which language people
|
|
read something in is most of what these figures are for.
|
|
count:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 1000
|
|
default: 1
|
|
description: |
|
|
For a frontend that batches rather than reporting each render.
|
|
Capped: the frontend is trusted to count its own renders, not to
|
|
invent an audience.
|
|
|
|
MissingUrlReport:
|
|
type: object
|
|
required: [path]
|
|
properties:
|
|
path:
|
|
type: string
|
|
maxLength: 500
|
|
pattern: "^/"
|
|
example: /proyectos/nombre-viejo
|
|
description: |
|
|
A path, not a URL. The frontend knows its own origin, and accepting
|
|
a full address would let a caller file a 404 against somebody
|
|
else's domain. Normalised on arrival: query strings and trailing
|
|
slashes are stripped, so `/algo/` and `/algo?utm_source=x` are one
|
|
address rather than three.
|
|
locale:
|
|
type: string
|
|
enum: [es, en, ca]
|
|
description: |
|
|
Which language was being rendered. Falls back to the negotiated
|
|
locale when absent, because it is half of the key that keeps one
|
|
address in two languages as two separate problems — they need two
|
|
different redirects.
|
|
referrer:
|
|
type: string
|
|
format: uri
|
|
maxLength: 500
|
|
nullable: true
|
|
description: |
|
|
Straight from the `Referer` header. The most useful field here: one
|
|
on our own domain means something we publish links to a dead
|
|
address, which the panel flags as an error rather than a warning.
|
|
|
|
Submission:
|
|
type: object
|
|
required: [name, email, message, consent]
|
|
properties:
|
|
name: { type: string, minLength: 2, maxLength: 120 }
|
|
email: { type: string, format: email, maxLength: 255 }
|
|
message:
|
|
type: string
|
|
minLength: 20
|
|
maxLength: 5000
|
|
description: |
|
|
The minimum length costs a real person nothing and rules out the
|
|
one-word submissions that are never anything but noise.
|
|
organisation:
|
|
type: string
|
|
maxLength: 160
|
|
nullable: true
|
|
description: Required when `kind` is `request`.
|
|
hours_per_week:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 40
|
|
nullable: true
|
|
description: |
|
|
Mirrors the call's own ask, so the two numbers can be compared
|
|
without reading the message.
|
|
links:
|
|
type: object
|
|
maxProperties: 5
|
|
additionalProperties: { type: string, format: uri }
|
|
example: { github: "https://github.com/ada", web: "https://ada.example" }
|
|
consent:
|
|
type: boolean
|
|
description: |
|
|
Must be `true`. The sender is not a user of this system, so their
|
|
agreement to have their details stored travels with the payload
|
|
rather than being assumed of the frontend.
|
|
website:
|
|
type: string
|
|
nullable: true
|
|
description: |
|
|
Honeypot — the field name comes from `SITE_HONEYPOT_FIELD`. Never
|
|
render it visibly. A submission carrying it is filed as spam
|
|
rather than dropped, and still answers 201, so a bot learns
|
|
nothing and a mistakenly-tripped human is recoverable.
|
|
|
|
SubmissionReceipt:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
kind: { type: string, enum: [contribute, request] }
|
|
status: { type: string, example: new }
|
|
duplicate:
|
|
type: boolean
|
|
description: True when this repeated an earlier submission.
|
|
message:
|
|
type: string
|
|
description: Confirmation text, already in the visitor's language.
|
|
example: Hemos recibido tu mensaje. Te contestamos en cuanto podamos.
|
|
|
|
PublicProject:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
locale: { type: string, example: es }
|
|
title: { type: string }
|
|
slug: { type: string }
|
|
tagline: { type: string, nullable: true }
|
|
description:
|
|
type: string
|
|
nullable: true
|
|
description: Detail endpoint only.
|
|
decisions:
|
|
type: string
|
|
nullable: true
|
|
description: |
|
|
Why the technical choices were made. Detail endpoint only.
|
|
postmortem:
|
|
type: string
|
|
nullable: true
|
|
description: |
|
|
What went wrong and what was learned. Detail endpoint only.
|
|
meta_title:
|
|
type: string
|
|
nullable: true
|
|
description: |
|
|
What a search engine or a chat preview should show instead of the
|
|
title. Detail endpoint only.
|
|
meta_description:
|
|
type: string
|
|
nullable: true
|
|
description: Detail endpoint only.
|
|
status: { type: string, enum: [idea, active, paused, completed, archived] }
|
|
is_featured: { type: boolean }
|
|
needs_help:
|
|
type: boolean
|
|
description: Flagged directly, or carrying at least one open call.
|
|
cover_image:
|
|
allOf: [{ $ref: "#/components/schemas/Image" }]
|
|
nullable: true
|
|
gallery:
|
|
type: array
|
|
description: |
|
|
Screenshots and before-and-after, in the order they were arranged
|
|
in the panel. **Detail endpoint only** — a listing of twenty
|
|
projects has no use for every screenshot of each one.
|
|
items:
|
|
allOf:
|
|
- $ref: "#/components/schemas/Image"
|
|
- type: object
|
|
properties:
|
|
caption:
|
|
type: string
|
|
nullable: true
|
|
description: |
|
|
Shown to everyone, unlike `alt`, which is read by
|
|
whoever cannot see the image. "Before" and "after"
|
|
are captions.
|
|
demo_url: { type: string, nullable: true }
|
|
client:
|
|
allOf: [{ $ref: "#/components/schemas/PublicClient" }]
|
|
nullable: true
|
|
description: |
|
|
The NGO or association the work serves. Used to be two free-text
|
|
fields, which made the same organisation two different things.
|
|
services:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicService" }
|
|
testimonials:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicTestimonial" }
|
|
talks:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicTalk" }
|
|
started_at: { type: string, format: date, nullable: true }
|
|
ended_at: { type: string, format: date, nullable: true }
|
|
published_at: { type: string, format: date-time, nullable: true }
|
|
unpublish_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
description: |
|
|
When the record leaves the site. Enforced by the query itself, so
|
|
it holds whether or not anything is running. Use it to expire a
|
|
cache downstream at the same moment.
|
|
updated_at: { type: string, format: date-time }
|
|
technologies:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicTechnology" }
|
|
members:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicMember" }
|
|
lead: { $ref: "#/components/schemas/PublicMember" }
|
|
calls:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicCall" }
|
|
repositories:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicRepository" }
|
|
taskstream:
|
|
type: object
|
|
nullable: true
|
|
description: "Its board in TaskStream, if it has one. `showcase_url` answers without a token: roadmap, releases, the sprint under way and tickets labelled good-first-issue."
|
|
properties:
|
|
key: { type: string, example: WEB }
|
|
url: { type: string, format: uri }
|
|
web_url: { type: string, format: uri }
|
|
showcase_url: { type: string, format: uri }
|
|
alternates: { $ref: "#/components/schemas/Alternates" }
|
|
path: { type: string, nullable: true, example: /proyectos/mi-web }
|
|
|
|
PublicMember:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: string }
|
|
slug: { type: string }
|
|
headline: { type: string, nullable: true }
|
|
bio: { type: string, nullable: true }
|
|
avatar:
|
|
allOf: [{ $ref: "#/components/schemas/Image" }]
|
|
nullable: true
|
|
github_username: { type: string, nullable: true }
|
|
links:
|
|
type: object
|
|
additionalProperties: { type: string }
|
|
example: { github: "https://github.com/ada", website: "https://ada.dev" }
|
|
is_founder: { type: boolean }
|
|
is_current:
|
|
type: boolean
|
|
description: False for people who have left; their work is kept.
|
|
joined_at: { type: string, format: date, nullable: true }
|
|
left_at: { type: string, format: date, nullable: true }
|
|
role:
|
|
type: string
|
|
nullable: true
|
|
description: Their role on a project, when nested under one.
|
|
technologies:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicTechnology" }
|
|
projects:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicProject" }
|
|
alternates: { $ref: "#/components/schemas/Alternates" }
|
|
path: { type: string, example: /equipo/ada }
|
|
|
|
PublicCall:
|
|
type: object
|
|
description: |
|
|
A call for contributors. The commitment fields are the point: time is
|
|
the scarce resource in a volunteer collective, and stating it plainly
|
|
is what lets someone decide.
|
|
properties:
|
|
id: { type: integer }
|
|
locale: { type: string }
|
|
title: { type: string }
|
|
slug: { type: string }
|
|
description: { type: string, nullable: true }
|
|
what_you_get:
|
|
type: string
|
|
nullable: true
|
|
description: The explicit exchange offered for unpaid work.
|
|
status: { type: string, enum: [open, filled, closed] }
|
|
hours_per_week: { type: integer, nullable: true, example: 4 }
|
|
duration_weeks: { type: integer, nullable: true, example: 8 }
|
|
total_hours: { type: integer, nullable: true, example: 32 }
|
|
seniority: { type: string, nullable: true, enum: [any, junior, mid, senior] }
|
|
closes_at: { type: string, format: date, nullable: true }
|
|
project: { $ref: "#/components/schemas/PublicProject" }
|
|
technologies:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicTechnology" }
|
|
alternates: { $ref: "#/components/schemas/Alternates" }
|
|
path: { type: string, nullable: true }
|
|
|
|
PublicSearchResult:
|
|
type: object
|
|
properties:
|
|
type: { type: string, enum: [page, project, service, call, member, technology] }
|
|
id: { type: integer }
|
|
title: { type: string }
|
|
excerpt: { type: string, description: Plain text around the first match. }
|
|
path: { type: string, nullable: true, example: /proyectos/mi-proyecto }
|
|
score: { type: number, description: Higher is a better match. Only comparable within one response. }
|
|
|
|
PublicTechnology:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
name:
|
|
type: string
|
|
description: Not translated — technology names are proper nouns.
|
|
example: Laravel
|
|
slug: { type: string }
|
|
category: { type: string, enum: [language, framework, datastore, infrastructure, tool, practice] }
|
|
color: { type: string, nullable: true }
|
|
icon: { type: string, nullable: true }
|
|
logo:
|
|
allOf: [{ $ref: "#/components/schemas/Image" }]
|
|
nullable: true
|
|
description: |
|
|
The technology's own logo. Its `alt` is the technology's name —
|
|
there is nothing to write and nothing to get wrong.
|
|
website: { type: string, nullable: true }
|
|
ring:
|
|
type: string
|
|
nullable: true
|
|
enum: [adopt, trial, assess, hold]
|
|
description: Radar placement. Null for technologies used but not placed.
|
|
description: { type: string, nullable: true }
|
|
radar_note:
|
|
type: string
|
|
nullable: true
|
|
description: Why it sits in that ring.
|
|
projects_count: { type: integer }
|
|
members_count: { type: integer }
|
|
|
|
PublicRepository:
|
|
type: object
|
|
description: Mirrored from the git provider and refreshed hourly.
|
|
properties:
|
|
id: { type: integer }
|
|
provider: { type: string, enum: [github, gitlab, gitea] }
|
|
full_name: { type: string, example: colectivo/web }
|
|
url: { type: string, nullable: true }
|
|
description: { type: string, nullable: true }
|
|
primary_language: { type: string, nullable: true }
|
|
languages:
|
|
type: object
|
|
additionalProperties: { type: integer }
|
|
description: Bytes of code per language.
|
|
topics:
|
|
type: array
|
|
items: { type: string }
|
|
license: { type: string, nullable: true }
|
|
stars: { type: integer }
|
|
forks: { type: integer }
|
|
contributors_count: { type: integer }
|
|
open_issues_count: { type: integer }
|
|
is_archived: { type: boolean }
|
|
is_dormant:
|
|
type: boolean
|
|
description: |
|
|
Nothing pushed for six months. Lets the frontend avoid presenting
|
|
dead work as live.
|
|
pushed_at: { type: string, format: date-time, nullable: true }
|
|
synced_at: { type: string, format: date-time, nullable: true }
|
|
good_first_issues:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicIssue" }
|
|
|
|
PublicIssue:
|
|
type: object
|
|
properties:
|
|
number: { type: integer }
|
|
title: { type: string }
|
|
url: { type: string }
|
|
labels:
|
|
type: array
|
|
items: { type: string }
|
|
is_good_first_issue: { type: boolean }
|
|
is_help_wanted: { type: boolean }
|
|
is_available:
|
|
type: boolean
|
|
description: Nobody has claimed it. A claimed issue is not an invitation.
|
|
comments: { type: integer }
|
|
created_at: { type: string, format: date-time, nullable: true }
|
|
repository:
|
|
type: object
|
|
properties:
|
|
full_name: { type: string }
|
|
url: { type: string, nullable: true }
|
|
primary_language: { type: string, nullable: true }
|
|
project:
|
|
type: object
|
|
nullable: true
|
|
properties:
|
|
title: { type: string }
|
|
slug: { type: string }
|
|
|
|
PublicService:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
locale: { type: string }
|
|
title: { type: string }
|
|
slug: { type: string }
|
|
tagline: { type: string, nullable: true }
|
|
description:
|
|
type: string
|
|
nullable: true
|
|
description: Detail endpoint only.
|
|
meta_title:
|
|
type: string
|
|
nullable: true
|
|
description: |
|
|
What a search engine or a chat preview should show instead of the
|
|
title. Detail endpoint only.
|
|
meta_description:
|
|
type: string
|
|
nullable: true
|
|
description: Detail endpoint only.
|
|
icon:
|
|
type: string
|
|
nullable: true
|
|
description: A Heroicons name, e.g. `heroicon-o-code-bracket`.
|
|
is_featured: { type: boolean }
|
|
position: { type: integer }
|
|
updated_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
description: |
|
|
So a cache downstream can tell whether its copy is still the
|
|
current one.
|
|
projects:
|
|
type: array
|
|
description: The published work that backs the offer. Detail endpoint only.
|
|
items: { $ref: "#/components/schemas/PublicProject" }
|
|
technologies:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicTechnology" }
|
|
testimonials:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/PublicTestimonial" }
|
|
alternates: { $ref: "#/components/schemas/Alternates" }
|
|
path: { type: string, nullable: true }
|
|
|
|
PublicClient:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
name:
|
|
type: string
|
|
description: A proper noun, so it is the same in every language.
|
|
slug: { type: string }
|
|
sector: { type: string, nullable: true }
|
|
url: { type: string, nullable: true }
|
|
logo:
|
|
allOf: [{ $ref: "#/components/schemas/Image" }]
|
|
nullable: true
|
|
description: Its alt text is the organisation's name.
|
|
projects_count: { type: integer }
|
|
|
|
PublicTestimonial:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
quote: { type: string }
|
|
original_locale:
|
|
type: string
|
|
description: The language it was actually said in.
|
|
is_original:
|
|
type: boolean
|
|
description: |
|
|
False when the quote you are reading was translated by us. Say so —
|
|
"translated from Spanish" — rather than implying they phrased it
|
|
that way.
|
|
author:
|
|
type: object
|
|
properties:
|
|
name: { type: string }
|
|
role: { type: string, nullable: true }
|
|
avatar:
|
|
allOf: [{ $ref: "#/components/schemas/Image" }]
|
|
nullable: true
|
|
given_at: { type: string, format: date, nullable: true }
|
|
client:
|
|
allOf: [{ $ref: "#/components/schemas/PublicClient" }]
|
|
nullable: true
|
|
|
|
PublicTalk:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
title:
|
|
type: string
|
|
description: |
|
|
Not translated: the name of a talk given once, in one language.
|
|
language: { type: string }
|
|
summary: { type: string, nullable: true }
|
|
event: { type: string }
|
|
city: { type: string, nullable: true }
|
|
held_at: { type: string, format: date, nullable: true }
|
|
video_url: { type: string, nullable: true }
|
|
slides_url: { type: string, nullable: true }
|
|
speaker:
|
|
allOf: [{ $ref: "#/components/schemas/PublicMember" }]
|
|
nullable: true
|
|
|
|
PublicPage:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
locale: { type: string }
|
|
title: { type: string, nullable: true }
|
|
slug: { type: string, nullable: true }
|
|
excerpt: { type: string, nullable: true }
|
|
body: { type: string, nullable: true }
|
|
meta_title: { type: string, nullable: true }
|
|
meta_description: { type: string, nullable: true }
|
|
status: { type: string }
|
|
is_featured: { type: boolean }
|
|
featured_image:
|
|
allOf: [{ $ref: "#/components/schemas/Image" }]
|
|
nullable: true
|
|
published_at: { type: string, format: date-time, nullable: true }
|
|
unpublish_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
description: |
|
|
When the record leaves the site. Enforced by the query itself, so
|
|
it holds whether or not anything is running. Use it to expire a
|
|
cache downstream at the same moment.
|
|
updated_at: { type: string, format: date-time }
|
|
category: { $ref: "#/components/schemas/PublicCategory" }
|
|
tags:
|
|
type: array
|
|
description: The ones named in this language, to link to the blog filtered by each.
|
|
items:
|
|
type: object
|
|
properties:
|
|
name: { type: string }
|
|
slug: { type: string }
|
|
byline:
|
|
type: object
|
|
nullable: true
|
|
description: The member who signs it, when they have a public profile.
|
|
properties:
|
|
name: { type: string }
|
|
slug: { type: string }
|
|
headline: { type: string, nullable: true }
|
|
avatar:
|
|
allOf: [{ $ref: "#/components/schemas/Image" }]
|
|
nullable: true
|
|
path: { type: string }
|
|
alternates: { $ref: "#/components/schemas/Alternates" }
|
|
path: { type: string, nullable: true }
|
|
|
|
PublicCategory:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: string, nullable: true }
|
|
slug: { type: string, nullable: true }
|
|
description: { type: string, nullable: true }
|
|
color: { type: string, nullable: true }
|
|
position: { type: integer }
|
|
pages_count: { type: integer }
|
|
|
|
Alternates:
|
|
type: object
|
|
description: |
|
|
The same record's path in each language that has a translation. Feed
|
|
these straight into `<link rel="alternate" hreflang>`. Languages with
|
|
no translation are absent, so no advertised URL ever 404s.
|
|
additionalProperties: { type: string }
|
|
example:
|
|
es: /proyectos/mi-web
|
|
en: /en/projects/my-web
|
|
ca: /ca/projectes/el-meu-web
|
|
TranslatedField:
|
|
description: |
|
|
Either a plain string (applies to the active language) or an object
|
|
keyed by locale.
|
|
oneOf:
|
|
- type: string
|
|
- type: object
|
|
additionalProperties: { type: string, nullable: true }
|
|
example: { es: "Inicio", en: "Home", ca: "Inici" }
|
|
|
|
Page:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
locale:
|
|
type: string
|
|
description: The language the fields below were resolved into.
|
|
example: es
|
|
title: { type: string, nullable: true }
|
|
slug: { type: string, nullable: true }
|
|
excerpt: { type: string, nullable: true }
|
|
body: { type: string, nullable: true }
|
|
meta_title: { type: string, nullable: true }
|
|
meta_description: { type: string, nullable: true }
|
|
status: { type: string, enum: [draft, review, published, archived] }
|
|
status_label: { type: string, description: Translated status name. }
|
|
is_featured: { type: boolean }
|
|
featured_image:
|
|
type: string
|
|
nullable: true
|
|
description: |
|
|
The stored path, because this is the editing API and it is what a
|
|
client PATCHes back. What was derived from it travels alongside.
|
|
featured_image_media:
|
|
allOf: [{ $ref: "#/components/schemas/Image" }]
|
|
nullable: true
|
|
readOnly: true
|
|
published_at: { type: string, format: date-time, nullable: true }
|
|
unpublish_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
description: |
|
|
When the record leaves the site. Enforced by the query itself, so
|
|
it holds whether or not anything is running. Use it to expire a
|
|
cache downstream at the same moment.
|
|
created_at: { type: string, format: date-time }
|
|
updated_at: { type: string, format: date-time }
|
|
deleted_at: { type: string, format: date-time, nullable: true }
|
|
category: { $ref: "#/components/schemas/Category" }
|
|
author: { $ref: "#/components/schemas/User" }
|
|
byline_member_id: { type: integer, nullable: true, description: The member who signs it on the site. }
|
|
tag_ids: { type: array, items: { type: integer } }
|
|
featured_image_alt:
|
|
type: string
|
|
nullable: true
|
|
description: |
|
|
What the image shows, resolved into the language above — unlike
|
|
`PageWrite`'s field of the same name, which carries every
|
|
language at once.
|
|
translations:
|
|
type: object
|
|
nullable: true
|
|
description: Present only when `?with=translations` was passed.
|
|
additionalProperties:
|
|
type: object
|
|
additionalProperties: { type: string }
|
|
|
|
PageWrite:
|
|
type: object
|
|
required: [title, slug]
|
|
properties:
|
|
title: { $ref: "#/components/schemas/TranslatedField" }
|
|
slug:
|
|
allOf: [{ $ref: "#/components/schemas/TranslatedField" }]
|
|
description: Unique per language. The same slug may be reused across languages.
|
|
excerpt: { $ref: "#/components/schemas/TranslatedField" }
|
|
body: { $ref: "#/components/schemas/TranslatedField" }
|
|
meta_title: { $ref: "#/components/schemas/TranslatedField" }
|
|
meta_description: { $ref: "#/components/schemas/TranslatedField" }
|
|
category_id: { type: integer, nullable: true }
|
|
byline_member_id: { type: integer, nullable: true, description: The member who signs it on the site. }
|
|
tag_ids: { type: array, items: { type: integer }, description: Replaced whole when sent. }
|
|
status: { type: string, enum: [draft, review, published, archived] }
|
|
featured_image:
|
|
type: string
|
|
nullable: true
|
|
description: Path returned by the upload, e.g. "pages/hero.jpg".
|
|
featured_image_alt:
|
|
allOf: [{ $ref: "#/components/schemas/TranslatedField" }]
|
|
description: |
|
|
What the image shows, per language. Belongs to the record rather
|
|
than to the file: the same image can need a different description
|
|
depending on where it is used.
|
|
is_featured: { type: boolean }
|
|
published_at: { type: string, format: date-time, nullable: true }
|
|
unpublish_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
description: Must be after `published_at`. When it arrives the record leaves the site on its own.
|
|
|
|
Project:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
locale: { type: string, description: The language the fields below were resolved into. }
|
|
title: { type: string, nullable: true }
|
|
slug: { type: string, nullable: true }
|
|
tagline: { type: string, nullable: true }
|
|
description: { type: string, nullable: true, description: HTML from the panel's rich editor. }
|
|
decisions: { type: string, nullable: true, description: HTML. Why the technical choices were made. }
|
|
postmortem: { type: string, nullable: true, description: HTML. What went wrong and what was learned. }
|
|
meta_title: { type: string, nullable: true }
|
|
meta_description: { type: string, nullable: true }
|
|
cover_image_alt: { type: string, nullable: true }
|
|
status: { type: string, enum: [idea, active, paused, completed, archived] }
|
|
is_featured: { type: boolean }
|
|
needs_help: { type: boolean }
|
|
demo_url: { type: string, nullable: true, format: uri }
|
|
taskstream_key: { type: string, nullable: true, description: "The project's key in TaskStream, when it has a board there." }
|
|
taskstream_url: { type: string, nullable: true, format: uri, readOnly: true, description: Where its TaskStream board opens. }
|
|
cover_image: { type: string, nullable: true, description: The stored path — what a client PATCHes back. }
|
|
cover_image_media:
|
|
allOf: [{ $ref: "#/components/schemas/Image" }]
|
|
nullable: true
|
|
readOnly: true
|
|
started_at: { type: string, format: date, nullable: true }
|
|
ended_at: { type: string, format: date, nullable: true }
|
|
published_at: { type: string, format: date-time, nullable: true, description: On the site from then on; null keeps it off. }
|
|
unpublish_at: { type: string, format: date-time, nullable: true }
|
|
position: { type: integer }
|
|
client_id: { type: integer, nullable: true }
|
|
lead_member_id: { type: integer, nullable: true }
|
|
technology_ids: { type: array, items: { type: integer } }
|
|
service_ids: { type: array, items: { type: integer } }
|
|
members:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: string }
|
|
role: { type: string, nullable: true }
|
|
created_at: { type: string, format: date-time }
|
|
updated_at: { type: string, format: date-time }
|
|
deleted_at: { type: string, format: date-time, nullable: true }
|
|
translations: { type: object, nullable: true, description: "Every language, with `?with=translations`." }
|
|
|
|
ProjectWrite:
|
|
type: object
|
|
required: [title, slug]
|
|
properties:
|
|
title: { $ref: "#/components/schemas/TranslatedField" }
|
|
slug:
|
|
allOf: [{ $ref: "#/components/schemas/TranslatedField" }]
|
|
description: Unique per language.
|
|
tagline: { $ref: "#/components/schemas/TranslatedField" }
|
|
description: { $ref: "#/components/schemas/TranslatedField" }
|
|
decisions: { $ref: "#/components/schemas/TranslatedField" }
|
|
postmortem: { $ref: "#/components/schemas/TranslatedField" }
|
|
meta_title: { $ref: "#/components/schemas/TranslatedField" }
|
|
meta_description: { $ref: "#/components/schemas/TranslatedField" }
|
|
cover_image_alt: { $ref: "#/components/schemas/TranslatedField" }
|
|
status: { type: string, enum: [idea, active, paused, completed, archived] }
|
|
is_featured: { type: boolean }
|
|
needs_help: { type: boolean }
|
|
demo_url: { type: string, format: uri, nullable: true }
|
|
taskstream:
|
|
type: boolean
|
|
description: On creation only. Also open a public board for it in TaskStream; see `POST /v1/projects`.
|
|
taskstream_key:
|
|
type: string
|
|
nullable: true
|
|
pattern: "^[A-Za-z][A-Za-z0-9]{1,9}$"
|
|
description: On creation only, with `taskstream`. The key in TaskStream; made from the slug when left out.
|
|
cover_image: { type: string, nullable: true, description: A path from `POST /v1/media` with `for=projects`. }
|
|
started_at: { type: string, format: date, nullable: true }
|
|
ended_at: { type: string, format: date, nullable: true }
|
|
published_at: { type: string, format: date-time, nullable: true, description: Requires `projects.publish`. }
|
|
unpublish_at: { type: string, format: date-time, nullable: true, description: Requires `projects.publish`. }
|
|
position: { type: integer, minimum: 0 }
|
|
client_id: { type: integer, nullable: true }
|
|
lead_member_id: { type: integer, nullable: true }
|
|
technology_ids: { type: array, items: { type: integer } }
|
|
service_ids: { type: array, items: { type: integer } }
|
|
members:
|
|
type: array
|
|
description: The whole team, replacing the previous one.
|
|
items:
|
|
type: object
|
|
required: [id]
|
|
properties:
|
|
id: { type: integer }
|
|
role: { type: string, nullable: true, maxLength: 120 }
|
|
|
|
ProjectCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Project" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
Member:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
locale: { type: string }
|
|
name: { type: string }
|
|
slug: { type: string }
|
|
headline: { type: string, nullable: true }
|
|
bio: { type: string, nullable: true, description: Plain text. }
|
|
avatar: { type: string, nullable: true, description: The stored path. }
|
|
avatar_media:
|
|
allOf: [{ $ref: "#/components/schemas/Image" }]
|
|
nullable: true
|
|
readOnly: true
|
|
email: { type: string, nullable: true, description: Never shown on the site. }
|
|
github_username: { type: string, nullable: true }
|
|
links: { type: object, additionalProperties: { type: string }, description: Label → URL. }
|
|
is_public: { type: boolean }
|
|
is_founder: { type: boolean }
|
|
joined_at: { type: string, format: date, nullable: true }
|
|
left_at: { type: string, format: date, nullable: true }
|
|
position: { type: integer }
|
|
technology_ids: { type: array, items: { type: integer } }
|
|
projects:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
title: { type: string, nullable: true }
|
|
role: { type: string, nullable: true }
|
|
created_at: { type: string, format: date-time }
|
|
updated_at: { type: string, format: date-time }
|
|
deleted_at: { type: string, format: date-time, nullable: true }
|
|
translations: { type: object, nullable: true, description: "Every language, with `?with=translations`." }
|
|
|
|
MemberWrite:
|
|
type: object
|
|
required: [name, slug]
|
|
properties:
|
|
name: { type: string, maxLength: 255 }
|
|
slug: { type: string, maxLength: 255, pattern: "^[A-Za-z0-9_-]+$" }
|
|
headline: { $ref: "#/components/schemas/TranslatedField" }
|
|
bio: { $ref: "#/components/schemas/TranslatedField" }
|
|
avatar: { type: string, nullable: true, description: A path from `POST /v1/media` with `for=members`. }
|
|
email: { type: string, format: email, nullable: true }
|
|
github_username: { type: string, nullable: true, maxLength: 120 }
|
|
links: { type: object, nullable: true, additionalProperties: { type: string, format: uri } }
|
|
is_public: { type: boolean }
|
|
is_founder: { type: boolean }
|
|
joined_at: { type: string, format: date, nullable: true }
|
|
left_at: { type: string, format: date, nullable: true }
|
|
position: { type: integer, minimum: 0 }
|
|
technology_ids: { type: array, items: { type: integer } }
|
|
|
|
MemberCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Member" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
Service:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
locale: { type: string }
|
|
title: { type: string, nullable: true }
|
|
slug: { type: string, nullable: true }
|
|
tagline: { type: string, nullable: true }
|
|
description: { type: string, nullable: true, description: Plain text. }
|
|
meta_title: { type: string, nullable: true }
|
|
meta_description: { type: string, nullable: true }
|
|
icon: { type: string, nullable: true }
|
|
is_featured: { type: boolean }
|
|
is_published: { type: boolean }
|
|
published_at: { type: string, format: date-time, nullable: true }
|
|
unpublish_at: { type: string, format: date-time, nullable: true }
|
|
position: { type: integer }
|
|
technology_ids: { type: array, items: { type: integer } }
|
|
project_ids: { type: array, items: { type: integer } }
|
|
created_at: { type: string, format: date-time }
|
|
updated_at: { type: string, format: date-time }
|
|
deleted_at: { type: string, format: date-time, nullable: true }
|
|
translations: { type: object, nullable: true, description: "Every language, with `?with=translations`." }
|
|
|
|
ServiceWrite:
|
|
type: object
|
|
required: [title, slug]
|
|
properties:
|
|
title: { $ref: "#/components/schemas/TranslatedField" }
|
|
slug:
|
|
allOf: [{ $ref: "#/components/schemas/TranslatedField" }]
|
|
description: Unique per language.
|
|
tagline: { $ref: "#/components/schemas/TranslatedField" }
|
|
description: { $ref: "#/components/schemas/TranslatedField" }
|
|
meta_title: { $ref: "#/components/schemas/TranslatedField" }
|
|
meta_description: { $ref: "#/components/schemas/TranslatedField" }
|
|
icon: { type: string, nullable: true, maxLength: 120, description: "An icon name, e.g. `support`." }
|
|
is_featured: { type: boolean }
|
|
position: { type: integer, minimum: 0 }
|
|
is_published: { type: boolean, description: Requires `services.publish`. }
|
|
published_at: { type: string, format: date-time, nullable: true, description: Requires `services.publish`. }
|
|
unpublish_at: { type: string, format: date-time, nullable: true, description: Requires `services.publish`. }
|
|
technology_ids: { type: array, items: { type: integer } }
|
|
project_ids: { type: array, items: { type: integer } }
|
|
|
|
ServiceCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Service" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
Client:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
locale: { type: string }
|
|
name: { type: string }
|
|
slug: { type: string }
|
|
sector: { type: string, nullable: true }
|
|
logo: { type: string, nullable: true, description: The stored path. }
|
|
logo_media:
|
|
allOf: [{ $ref: "#/components/schemas/Image" }]
|
|
nullable: true
|
|
readOnly: true
|
|
url: { type: string, format: uri, nullable: true }
|
|
is_public: { type: boolean }
|
|
position: { type: integer }
|
|
projects_count: { type: integer }
|
|
created_at: { type: string, format: date-time }
|
|
updated_at: { type: string, format: date-time }
|
|
deleted_at: { type: string, format: date-time, nullable: true }
|
|
translations: { type: object, nullable: true, description: "Every language, with `?with=translations`." }
|
|
|
|
ClientWrite:
|
|
type: object
|
|
required: [name, slug]
|
|
properties:
|
|
name: { type: string, maxLength: 255, description: A proper noun; not translated. }
|
|
slug: { type: string, maxLength: 255, pattern: "^[A-Za-z0-9_-]+$" }
|
|
sector: { $ref: "#/components/schemas/TranslatedField" }
|
|
logo: { type: string, nullable: true, description: A path from `POST /v1/media` with `for=clients`. }
|
|
url: { type: string, format: uri, nullable: true }
|
|
is_public: { type: boolean }
|
|
position: { type: integer, minimum: 0 }
|
|
|
|
ClientCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Client" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
Testimonial:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
locale: { type: string }
|
|
quote: { type: string, nullable: true }
|
|
author_role: { type: string, nullable: true }
|
|
original_locale: { type: string, description: The language the person spoke in. }
|
|
author_name: { type: string }
|
|
avatar: { type: string, nullable: true, description: The stored path. }
|
|
avatar_media:
|
|
allOf: [{ $ref: "#/components/schemas/Image" }]
|
|
nullable: true
|
|
readOnly: true
|
|
client_id: { type: integer, nullable: true }
|
|
project_id: { type: integer, nullable: true }
|
|
service_id: { type: integer, nullable: true }
|
|
is_public: { type: boolean }
|
|
position: { type: integer }
|
|
given_at: { type: string, format: date, nullable: true }
|
|
created_at: { type: string, format: date-time }
|
|
updated_at: { type: string, format: date-time }
|
|
deleted_at: { type: string, format: date-time, nullable: true }
|
|
translations: { type: object, nullable: true, description: "Every language, with `?with=translations`." }
|
|
|
|
TestimonialWrite:
|
|
type: object
|
|
required: [quote, original_locale, author_name]
|
|
properties:
|
|
quote: { $ref: "#/components/schemas/TranslatedField" }
|
|
original_locale: { type: string, description: A supported language code. }
|
|
author_name: { type: string, maxLength: 255 }
|
|
author_role: { $ref: "#/components/schemas/TranslatedField" }
|
|
avatar: { type: string, nullable: true, description: A path from `POST /v1/media` with `for=testimonials`. }
|
|
client_id: { type: integer, nullable: true }
|
|
project_id: { type: integer, nullable: true }
|
|
service_id: { type: integer, nullable: true }
|
|
is_public: { type: boolean }
|
|
position: { type: integer, minimum: 0 }
|
|
given_at: { type: string, format: date, nullable: true }
|
|
|
|
TestimonialCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Testimonial" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
Talk:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
locale: { type: string }
|
|
title: { type: string }
|
|
language: { type: string }
|
|
summary: { type: string, nullable: true }
|
|
event: { type: string }
|
|
city: { type: string, nullable: true }
|
|
held_at: { type: string, format: date, nullable: true }
|
|
video_url: { type: string, format: uri, nullable: true }
|
|
slides_url: { type: string, format: uri, nullable: true }
|
|
member_id: { type: integer, nullable: true }
|
|
project_id: { type: integer, nullable: true }
|
|
is_public: { type: boolean }
|
|
created_at: { type: string, format: date-time }
|
|
updated_at: { type: string, format: date-time }
|
|
translations: { type: object, nullable: true, description: "Every language, with `?with=translations`." }
|
|
|
|
TalkWrite:
|
|
type: object
|
|
required: [title, language, event]
|
|
properties:
|
|
title: { type: string, maxLength: 255, description: Not translated. }
|
|
language: { type: string, description: A supported language code. }
|
|
summary: { $ref: "#/components/schemas/TranslatedField" }
|
|
event: { type: string, maxLength: 255 }
|
|
city: { type: string, nullable: true, maxLength: 120 }
|
|
held_at: { type: string, format: date, nullable: true }
|
|
video_url: { type: string, format: uri, nullable: true }
|
|
slides_url: { type: string, format: uri, nullable: true }
|
|
member_id: { type: integer, nullable: true }
|
|
project_id: { type: integer, nullable: true }
|
|
is_public: { type: boolean }
|
|
|
|
TalkCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Talk" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
Call:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
locale: { type: string }
|
|
title: { type: string, nullable: true }
|
|
slug: { type: string, nullable: true }
|
|
description: { type: string, nullable: true }
|
|
what_you_get: { type: string, nullable: true }
|
|
status: { type: string, enum: [open, filled, closed] }
|
|
hours_per_week: { type: integer, nullable: true }
|
|
duration_weeks: { type: integer, nullable: true }
|
|
seniority: { type: string, nullable: true, enum: [any, junior, mid, senior] }
|
|
opens_at: { type: string, format: date, nullable: true }
|
|
closes_at: { type: string, format: date, nullable: true }
|
|
project_id: { type: integer, nullable: true }
|
|
technology_ids: { type: array, items: { type: integer } }
|
|
applications_count: { type: integer }
|
|
created_at: { type: string, format: date-time }
|
|
updated_at: { type: string, format: date-time }
|
|
translations: { type: object, nullable: true, description: "Every language, with `?with=translations`." }
|
|
|
|
CallWrite:
|
|
type: object
|
|
required: [title, slug]
|
|
properties:
|
|
title: { $ref: "#/components/schemas/TranslatedField" }
|
|
slug:
|
|
allOf: [{ $ref: "#/components/schemas/TranslatedField" }]
|
|
description: Unique per language.
|
|
description: { $ref: "#/components/schemas/TranslatedField" }
|
|
what_you_get: { $ref: "#/components/schemas/TranslatedField" }
|
|
status: { type: string, enum: [open, filled, closed], default: open }
|
|
hours_per_week: { type: integer, nullable: true, minimum: 1, maximum: 40 }
|
|
duration_weeks: { type: integer, nullable: true, minimum: 1 }
|
|
seniority: { type: string, nullable: true, enum: [any, junior, mid, senior] }
|
|
opens_at: { type: string, format: date, nullable: true }
|
|
closes_at: { type: string, format: date, nullable: true }
|
|
project_id: { type: integer, nullable: true }
|
|
technology_ids: { type: array, items: { type: integer } }
|
|
|
|
CallCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Call" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
Application:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
kind: { type: string, enum: [contribute, request] }
|
|
status: { type: string, enum: [new, contacted, accepted, declined, spam] }
|
|
call_id: { type: integer, nullable: true }
|
|
subject: { type: string, description: "The call's title, or a general enquiry." }
|
|
name: { type: string }
|
|
email: { type: string, format: email }
|
|
organisation: { type: string, nullable: true }
|
|
message: { type: string }
|
|
hours_per_week: { type: integer, nullable: true }
|
|
links: { type: object, additionalProperties: { type: string }, description: "The links the sender gave, keyed as they were sent." }
|
|
locale: { type: string, description: The language it was written in. }
|
|
notes: { type: string, nullable: true, description: Private to the collective. }
|
|
handled_by:
|
|
type: object
|
|
nullable: true
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: string }
|
|
handled_at: { type: string, format: date-time, nullable: true }
|
|
created_at: { type: string, format: date-time }
|
|
updated_at: { type: string, format: date-time }
|
|
deleted_at: { type: string, format: date-time, nullable: true }
|
|
|
|
ApplicationWrite:
|
|
type: object
|
|
properties:
|
|
status: { type: string, enum: [new, contacted, accepted, declined, spam] }
|
|
notes: { type: string, nullable: true, maxLength: 10000 }
|
|
|
|
ApplicationCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Application" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
Redirect:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
from: { type: string }
|
|
to: { type: string }
|
|
status: { type: integer, enum: [301, 302] }
|
|
locale: { type: string, nullable: true, description: Null applies in every language. }
|
|
is_enabled: { type: boolean }
|
|
automatic: { type: boolean, description: Left behind by a slug rename. }
|
|
hits: { type: integer }
|
|
last_used_at: { type: string, format: date-time, nullable: true }
|
|
created_at: { type: string, format: date-time }
|
|
updated_at: { type: string, format: date-time }
|
|
|
|
RedirectWrite:
|
|
type: object
|
|
required: [from, to]
|
|
properties:
|
|
from: { type: string, maxLength: 500 }
|
|
to: { type: string, maxLength: 500, description: Different from `from`. }
|
|
status: { type: integer, enum: [301, 302], default: 301 }
|
|
locale: { type: string, nullable: true }
|
|
is_enabled: { type: boolean, default: true }
|
|
|
|
RedirectCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Redirect" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
MissingUrl:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
path: { type: string }
|
|
locale: { type: string }
|
|
referrer: { type: string, nullable: true }
|
|
internal_referrer: { type: boolean, description: "A page of our own links here — more urgent than someone else's stale link." }
|
|
status: { type: string, enum: [pending, resolved, ignored] }
|
|
hits: { type: integer }
|
|
first_seen_at: { type: string, format: date-time, nullable: true }
|
|
last_seen_at: { type: string, format: date-time, nullable: true }
|
|
resolved_at: { type: string, format: date-time, nullable: true }
|
|
|
|
MissingUrlWrite:
|
|
type: object
|
|
required: [status]
|
|
properties:
|
|
status: { type: string, enum: [pending, ignored] }
|
|
|
|
MissingUrlCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/MissingUrl" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
Repository:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
provider: { type: string, enum: [gitea, github, gitlab] }
|
|
full_name: { type: string, description: "`owner/name`." }
|
|
project_id: { type: integer, nullable: true }
|
|
is_primary: { type: boolean }
|
|
show_publicly: { type: boolean }
|
|
html_url: { type: string, nullable: true }
|
|
description: { type: string, nullable: true }
|
|
homepage: { type: string, nullable: true }
|
|
default_branch: { type: string, nullable: true }
|
|
primary_language: { type: string, nullable: true }
|
|
license: { type: string, nullable: true }
|
|
topics: { type: array, items: { type: string } }
|
|
stars: { type: integer }
|
|
forks: { type: integer }
|
|
open_issues_count: { type: integer }
|
|
is_archived: { type: boolean }
|
|
pushed_at: { type: string, format: date-time, nullable: true }
|
|
last_synced_at: { type: string, format: date-time, nullable: true }
|
|
sync_error: { type: string, nullable: true }
|
|
sync_failures: { type: integer }
|
|
created_at: { type: string, format: date-time }
|
|
updated_at: { type: string, format: date-time }
|
|
|
|
RepositoryWrite:
|
|
type: object
|
|
required: [provider, full_name]
|
|
properties:
|
|
provider: { type: string, enum: [gitea, github, gitlab] }
|
|
full_name: { type: string, maxLength: 255, description: "`owner/name`, unique per provider." }
|
|
project_id: { type: integer, nullable: true }
|
|
is_primary: { type: boolean }
|
|
show_publicly: { type: boolean }
|
|
|
|
RepositoryCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Repository" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
Setting:
|
|
type: object
|
|
properties:
|
|
key: { type: string }
|
|
value: { nullable: true, description: Any JSON value. }
|
|
updated_at: { type: string, format: date-time, nullable: true }
|
|
|
|
Maintenance:
|
|
type: object
|
|
properties:
|
|
enabled: { type: boolean, description: False once `until` has passed. }
|
|
message: { type: object, additionalProperties: { type: string }, description: Language → text shown to visitors. }
|
|
until: { type: string, format: date-time, nullable: true }
|
|
started_at: { type: string, format: date-time, nullable: true }
|
|
|
|
Revision:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
event: { type: string, enum: [created, updated, published, scheduled, unpublished, deleted, restored, erased] }
|
|
author:
|
|
type: object
|
|
properties:
|
|
id: { type: integer, nullable: true }
|
|
name: { type: string }
|
|
changes:
|
|
type: object
|
|
description: "Field → `{old, new}`. Translatable fields carry every language."
|
|
additionalProperties:
|
|
type: object
|
|
properties:
|
|
old: { nullable: true }
|
|
new: { nullable: true }
|
|
created_at: { type: string, format: date-time }
|
|
|
|
RevisionCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Revision" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
ReviewComment:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
event: { type: string, enum: [requested, returned, published, commented] }
|
|
body: { type: string, nullable: true }
|
|
author:
|
|
type: object
|
|
properties:
|
|
id: { type: integer, nullable: true }
|
|
name: { type: string, nullable: true }
|
|
created_at: { type: string, format: date-time }
|
|
|
|
ReviewCommentCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/ReviewComment" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
ReviewState:
|
|
type: object
|
|
properties:
|
|
type: { type: string, enum: [pages, projects, services] }
|
|
id: { type: integer }
|
|
title: { type: string, nullable: true }
|
|
published: { type: boolean, description: On the site right now. }
|
|
published_at: { type: string, format: date-time, nullable: true, description: In the future when scheduled. }
|
|
awaiting_review: { type: boolean }
|
|
requested_at: { type: string, format: date-time, nullable: true }
|
|
requested_by: { $ref: "#/components/schemas/UserRef" }
|
|
reviewer: { $ref: "#/components/schemas/UserRef" }
|
|
|
|
UserRef:
|
|
type: object
|
|
nullable: true
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: string }
|
|
|
|
PreviewLinks:
|
|
type: object
|
|
properties:
|
|
links: { type: object, additionalProperties: { type: string, format: uri }, description: Language → link. }
|
|
expires_at: { type: string, format: date-time }
|
|
|
|
HealthFinding:
|
|
type: object
|
|
properties:
|
|
key: { type: string, example: missing-translations }
|
|
severity: { type: string, enum: [ok, warning, error] }
|
|
count: { type: integer }
|
|
title: { type: string }
|
|
description: { type: string }
|
|
samples: { type: array, items: { type: string }, description: Up to three examples. }
|
|
panel_url: { type: string, nullable: true, description: Where to fix it in the panel. }
|
|
|
|
PendingTranslation:
|
|
type: object
|
|
properties:
|
|
type: { type: string, enum: [pages, projects, services, calls] }
|
|
id: { type: integer }
|
|
title: { type: string }
|
|
missing: { type: array, items: { type: string }, description: Languages with nothing written. }
|
|
stale: { type: object, additionalProperties: { type: string, format: date-time }, description: Language → since when it has been behind. }
|
|
|
|
BatchOutcome:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
index: { type: integer }
|
|
method: { type: string }
|
|
path: { type: string }
|
|
status: { type: integer, description: The HTTP status the operation answered with. }
|
|
body: { nullable: true, description: What the operation answered. }
|
|
meta:
|
|
type: object
|
|
properties:
|
|
committed: { type: boolean, description: False when an atomic run or a dry run undid everything. }
|
|
succeeded: { type: integer }
|
|
failed: { type: integer }
|
|
dry_run: { type: boolean, nullable: true }
|
|
|
|
ApiToken:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: string }
|
|
prefix: { type: string, description: "The first characters, to tell tokens apart." }
|
|
abilities: { type: array, items: { type: string } }
|
|
owner:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: string, nullable: true }
|
|
status: { type: string, enum: [active, expired, revoked] }
|
|
expires_at: { type: string, format: date-time }
|
|
last_used_at: { type: string, format: date-time, nullable: true }
|
|
revoked_at: { type: string, format: date-time, nullable: true }
|
|
created_at: { type: string, format: date-time }
|
|
|
|
ApiTokenCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/ApiToken" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
Webhook:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: string }
|
|
url: { type: string, format: uri }
|
|
events: { type: array, items: { type: string } }
|
|
include_personal_data: { type: boolean }
|
|
is_active: { type: boolean }
|
|
consecutive_failures: { type: integer }
|
|
last_delivered_at: { type: string, format: date-time, nullable: true }
|
|
created_at: { type: string, format: date-time }
|
|
updated_at: { type: string, format: date-time }
|
|
|
|
WebhookWrite:
|
|
type: object
|
|
required: [name, url, events]
|
|
properties:
|
|
name: { type: string, maxLength: 120 }
|
|
url: { type: string, format: uri, description: "HTTPS, to a public address." }
|
|
events:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: "An event, `*`, `{resource}.*` or `*.{action}`."
|
|
example: projects.*
|
|
secret: { type: string, minLength: 16, description: On creation only; generated when left out. }
|
|
rotate_secret: { type: boolean, description: On update only. }
|
|
include_personal_data: { type: boolean, default: false }
|
|
is_active: { type: boolean }
|
|
|
|
WebhookCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Webhook" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
WebhookDelivery:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
uuid: { type: string, description: "Sent as `X-Webhook-Delivery`." }
|
|
event: { type: string }
|
|
status: { type: string, enum: [pending, succeeded, failed] }
|
|
attempts: { type: integer }
|
|
response_status: { type: integer, nullable: true }
|
|
error: { type: string, nullable: true }
|
|
payload: { type: object, additionalProperties: true }
|
|
delivered_at: { type: string, format: date-time, nullable: true }
|
|
created_at: { type: string, format: date-time }
|
|
|
|
WebhookDeliveryCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/WebhookDelivery" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
AuditLog:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
action: { type: string, example: pages.updated }
|
|
user:
|
|
type: object
|
|
properties:
|
|
id: { type: integer, nullable: true }
|
|
name: { type: string, nullable: true }
|
|
via: { type: string, enum: [panel, api, token, web, console] }
|
|
api_token_id: { type: integer, nullable: true, description: The machine token used. }
|
|
ip: { type: string, nullable: true }
|
|
subject:
|
|
type: object
|
|
nullable: true
|
|
properties:
|
|
type: { type: string }
|
|
id: { type: string }
|
|
label: { type: string, nullable: true }
|
|
changes:
|
|
type: object
|
|
nullable: true
|
|
additionalProperties: true
|
|
description: "Field → `{old, new}` on an update; the values on a creation. Long values are cut short."
|
|
created_at: { type: string, format: date-time }
|
|
|
|
AuditLogCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/AuditLog" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
ApplicationReply:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
subject: { type: string }
|
|
body: { type: string }
|
|
channel: { type: string, enum: [mail, mailto], description: "`mail` when sent from here; `mailto` when handed to the replier's mail program." }
|
|
author:
|
|
type: object
|
|
properties:
|
|
id: { type: integer, nullable: true }
|
|
name: { type: string, nullable: true }
|
|
created_at: { type: string, format: date-time }
|
|
|
|
ReplyTemplate:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: string }
|
|
kind: { type: string, nullable: true, enum: [contribute, request] }
|
|
subject: { type: object, additionalProperties: { type: string }, description: "Language → text. `{name}`, `{subject}` and `{sender}` are filled in." }
|
|
body: { type: object, additionalProperties: { type: string } }
|
|
|
|
Tag:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: object, additionalProperties: { type: string }, description: Language → name. }
|
|
slug: { type: object, additionalProperties: { type: string } }
|
|
pages_count: { type: integer }
|
|
|
|
TagWrite:
|
|
type: object
|
|
required: [name, slug]
|
|
properties:
|
|
name: { $ref: "#/components/schemas/TranslatedField" }
|
|
slug:
|
|
allOf: [{ $ref: "#/components/schemas/TranslatedField" }]
|
|
description: Unique per language.
|
|
|
|
MediaFor:
|
|
type: string
|
|
enum: [pages, technologies, projects, members, clients, testimonials]
|
|
default: pages
|
|
description: The kind of record the image is for.
|
|
|
|
Technology:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
locale: { type: string, description: The language the prose below was resolved into. }
|
|
name: { type: string, description: A proper noun — the same in every language. }
|
|
slug: { type: string }
|
|
description: { type: string, nullable: true }
|
|
radar_note: { type: string, nullable: true, description: Why it sits in its ring. }
|
|
category: { type: string, enum: [language, framework, datastore, infrastructure, tool, practice] }
|
|
radar_ring:
|
|
type: string
|
|
enum: [adopt, trial, assess, hold]
|
|
nullable: true
|
|
description: Null keeps it off the radar; projects can still use it.
|
|
color: { type: string, nullable: true, example: "#DD0031" }
|
|
logo:
|
|
type: string
|
|
nullable: true
|
|
description: The stored path — what `POST /v1/media` answered — and what a client PATCHes back.
|
|
logo_media:
|
|
allOf: [{ $ref: "#/components/schemas/Image" }]
|
|
nullable: true
|
|
readOnly: true
|
|
website: { type: string, nullable: true, format: uri }
|
|
position: { type: integer }
|
|
projects_count: { type: integer }
|
|
created_at: { type: string, format: date-time }
|
|
updated_at: { type: string, format: date-time }
|
|
translations:
|
|
type: object
|
|
nullable: true
|
|
description: Every language of `description` and `radar_note`, with `?with=translations`.
|
|
|
|
TechnologyWrite:
|
|
type: object
|
|
required: [name, slug]
|
|
properties:
|
|
name: { type: string, maxLength: 120 }
|
|
slug: { type: string, maxLength: 120, pattern: "^[A-Za-z0-9_-]+$" }
|
|
description: { $ref: "#/components/schemas/TranslatedField" }
|
|
radar_note: { $ref: "#/components/schemas/TranslatedField" }
|
|
category:
|
|
type: string
|
|
enum: [language, framework, datastore, infrastructure, tool, practice]
|
|
default: tool
|
|
radar_ring: { type: string, enum: [adopt, trial, assess, hold], nullable: true }
|
|
color: { type: string, nullable: true, pattern: "^#[0-9a-fA-F]{6}$" }
|
|
logo:
|
|
type: string
|
|
nullable: true
|
|
description: A path from `POST /v1/media`, uploaded with `for=technologies`.
|
|
website: { type: string, format: uri, nullable: true, maxLength: 255 }
|
|
position: { type: integer, minimum: 0 }
|
|
|
|
TechnologyCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Technology" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
Media:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
path:
|
|
type: string
|
|
description: What a record stores, e.g. as a page's `featured_image`.
|
|
example: pages/Xk3fQ9sLw2PmN8vR1tY6uZ0aB4cD7eF5gH2jK9mN.jpg
|
|
url: { type: string, format: uri, description: Absolute. }
|
|
mime: { type: string, nullable: true, example: image/jpeg }
|
|
size: { type: integer, nullable: true, description: Bytes. }
|
|
width: { type: integer, nullable: true }
|
|
height: { type: integer, nullable: true }
|
|
processed:
|
|
type: boolean
|
|
description: |
|
|
Whether the queue has derived the smaller versions and the
|
|
placeholder yet. The image serves either way.
|
|
error: { type: string, nullable: true, description: "Why processing failed, when it did." }
|
|
referenced:
|
|
type: boolean
|
|
nullable: true
|
|
description: Whether anything points at it. Sent by the library only, not on upload.
|
|
created_at: { type: string, format: date-time }
|
|
|
|
MediaCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Media" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
Category:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
locale: { type: string, example: es }
|
|
name: { type: string, nullable: true }
|
|
slug: { type: string, nullable: true }
|
|
description: { type: string, nullable: true }
|
|
color: { type: string, nullable: true, example: "#4f46e5" }
|
|
position: { type: integer }
|
|
is_visible: { type: boolean }
|
|
pages_count: { type: integer }
|
|
created_at: { type: string, format: date-time }
|
|
updated_at: { type: string, format: date-time }
|
|
translations:
|
|
type: object
|
|
nullable: true
|
|
description: Present only when `?with=translations` was passed.
|
|
additionalProperties:
|
|
type: object
|
|
additionalProperties: { type: string }
|
|
|
|
CategoryWrite:
|
|
type: object
|
|
required: [name, slug]
|
|
properties:
|
|
name: { $ref: "#/components/schemas/TranslatedField" }
|
|
slug: { $ref: "#/components/schemas/TranslatedField" }
|
|
description: { $ref: "#/components/schemas/TranslatedField" }
|
|
color: { type: string, pattern: '^#[0-9a-fA-F]{6}$' }
|
|
position: { type: integer, minimum: 0 }
|
|
is_visible: { type: boolean }
|
|
|
|
User:
|
|
type: object
|
|
properties:
|
|
id: { type: integer, description: Local id. }
|
|
oauth_id: { type: integer, nullable: true, description: This person's id on the identity server. }
|
|
name: { type: string }
|
|
email: { type: string, format: email }
|
|
avatar_url: { type: string, nullable: true }
|
|
locale: { type: string, nullable: true, enum: [es, en, ca] }
|
|
is_active: { type: boolean }
|
|
last_login_at: { type: string, format: date-time, nullable: true }
|
|
created_at: { type: string, format: date-time }
|
|
roles:
|
|
type: array
|
|
items: { type: string }
|
|
description: Local roles — what this person may do in this app.
|
|
example: [editor]
|
|
permissions:
|
|
type: array
|
|
items: { type: string }
|
|
nullable: true
|
|
description: Effective local permissions. Returned by `/v1/me` only.
|
|
remote_roles:
|
|
type: array
|
|
items: { type: string }
|
|
description: |
|
|
Roles reported by the identity server. Informational only; they
|
|
grant nothing here.
|
|
|
|
Role:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: string, example: editor }
|
|
guard_name: { type: string, example: web }
|
|
users_count: { type: integer }
|
|
permissions:
|
|
type: array
|
|
items: { type: string }
|
|
created_at: { type: string, format: date-time }
|
|
|
|
PasswordGrantRequest:
|
|
type: object
|
|
required: [grant_type, username, password]
|
|
properties:
|
|
grant_type: { type: string, enum: [password] }
|
|
username: { type: string, format: email }
|
|
password: { type: string, format: password }
|
|
scope: { type: string, example: "account:read" }
|
|
|
|
RefreshGrantRequest:
|
|
type: object
|
|
required: [grant_type, refresh_token]
|
|
properties:
|
|
grant_type: { type: string, enum: [refresh_token] }
|
|
refresh_token: { type: string }
|
|
|
|
TokenResponse:
|
|
type: object
|
|
properties:
|
|
access_token: { type: string }
|
|
refresh_token: { type: string, nullable: true }
|
|
token_type: { type: string, example: Bearer }
|
|
expires_at: { type: string, format: date-time, nullable: true }
|
|
scopes:
|
|
type: array
|
|
items: { type: string }
|
|
user:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: string }
|
|
email: { type: string }
|
|
roles:
|
|
type: array
|
|
items: { type: string }
|
|
permissions:
|
|
type: array
|
|
items: { type: string }
|
|
|
|
PaginationMeta:
|
|
type: object
|
|
description: |
|
|
Laravel's own paginator shape, sent as-is — nothing on this side
|
|
trims it down, so this describes exactly what `->paginate()` sends
|
|
rather than a curated subset of it.
|
|
properties:
|
|
current_page: { type: integer }
|
|
from: { type: integer, nullable: true, description: Null on an empty page. }
|
|
last_page: { type: integer }
|
|
links:
|
|
type: array
|
|
description: One entry per page number shown by a "1 2 3 … 9" control, plus prev/next.
|
|
items:
|
|
type: object
|
|
properties:
|
|
url: { type: string, nullable: true }
|
|
label: { type: string, description: "Already translated — e.g. «Anterior», not «Previous»." }
|
|
page: { type: integer, nullable: true }
|
|
active: { type: boolean }
|
|
path: { type: string }
|
|
per_page: { type: integer }
|
|
to: { type: integer, nullable: true, description: Null on an empty page. }
|
|
total: { type: integer }
|
|
|
|
PaginationLinks:
|
|
type: object
|
|
description: The four addresses a client actually navigates with, ready to use verbatim.
|
|
properties:
|
|
first: { type: string }
|
|
last: { type: string }
|
|
prev: { type: string, nullable: true }
|
|
next: { type: string, nullable: true }
|
|
|
|
PageCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Page" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|
|
|
|
CategoryCollection:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Category" }
|
|
links: { $ref: "#/components/schemas/PaginationLinks" }
|
|
meta: { $ref: "#/components/schemas/PaginationMeta" }
|