Manage clients, testimonials and talks through the API

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 19:38:58 +02:00
parent 96747abdc3
commit b02464a7ed
15 changed files with 1255 additions and 7 deletions

View File

@@ -112,6 +112,12 @@ tags:
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: Technologies
description: What the collective uses, and its place on the tech radar
- name: Users
@@ -1645,6 +1651,352 @@ paths:
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/media:
post:
tags: [Media]
@@ -1665,8 +2017,9 @@ paths:
removed. 20 MB at most.
`for` says which kind of record the image is for — `pages` (the
default) or `technologies` — which decides where it is stored and whose
permission it takes.
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
@@ -2872,9 +3225,147 @@ components:
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" }
MediaFor:
type: string
enum: [pages, technologies, projects, members]
enum: [pages, technologies, projects, members, clients, testimonials]
default: pages
description: The kind of record the image is for.