4487 lines
158 KiB
YAML
4487 lines
158 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`). This
|
|
application does not issue tokens of its own; it validates the ones the
|
|
identity server issued.
|
|
|
|
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.
|
|
|
|
## 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: Users
|
|
description: Local user administration
|
|
- name: Media
|
|
description: 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/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: 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/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/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`.
|
|
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" }
|
|
|
|
/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/media:
|
|
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/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.
|
|
|
|
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" }
|
|
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 }
|
|
|
|
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" }
|
|
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" }
|
|
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 }
|
|
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 }
|
|
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 }
|
|
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 }
|
|
|
|
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:
|
|
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.
|
|
|
|
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" }
|