openapi: 3.1.0

info:

title: Federated Personal Digital Platform API

version: 0.1.0

summary: Target API contract for the FPDP MVP

description: |

Bilingual-ready REST contract for FPDP. This specification describes the

intended MVP API; most HTTP operations are not implemented in the current

lightweight repository yet.

contact:

name: FPDP Maintainers

servers:

- url: http://localhost

description: Local development

tags:

- name: System

- name: Authentication

- name: Profiles

- name: Content

- name: External Sources

- name: Commerce

- name: Payments

- name: Administration

- name: Federation

paths:

/api/v1/health:

get:

tags: [System]

operationId: getHealth

summary: Get application health

security: []

responses:

'200':

description: Application is available

content:

application/json:

schema:

$ref: '#/components/schemas/HealthResponse'

'503':

$ref: '#/components/responses/ServiceUnavailable'

/api/v1/auth/register:

post:

tags: [Authentication]

operationId: registerOwner

summary: Register an owner and initial node

security: []

requestBody:

required: true

content:

application/json:

schema:

$ref: '#/components/schemas/RegisterRequest'

responses:

'201':

description: Owner and node created

content:

application/json:

schema:

$ref: '#/components/schemas/AuthResponse'

'409':

$ref: '#/components/responses/Conflict'

'422':

$ref: '#/components/responses/ValidationError'

/api/v1/auth/login:

post:

tags: [Authentication]

operationId: login

summary: Authenticate with email and password

security: []

requestBody:

required: true

content:

application/json:

schema:

$ref: '#/components/schemas/LoginRequest'

responses:

'200':

description: Authentication succeeded

content:

application/json:

schema:

$ref: '#/components/schemas/AuthResponse'

'401':

$ref: '#/components/responses/Unauthorized'

'422':

$ref: '#/components/responses/ValidationError'

/api/v1/auth/logout:

post:

tags: [Authentication]

operationId: logout

summary: Revoke the current access token

responses:

'204':

description: Token revoked

'401':

$ref: '#/components/responses/Unauthorized'

/api/v1/me:

get:

tags: [Authentication]

operationId: getCurrentUser

summary: Get the authenticated user and node context

responses:

'200':

description: Current user context

content:

application/json:

schema:

$ref: '#/components/schemas/MeResponse'

'401':

$ref: '#/components/responses/Unauthorized'

/api/v1/profiles/{handle}:

parameters:

- $ref: '#/components/parameters/Handle'

get:

tags: [Profiles]

operationId: getPublicProfile

summary: Get a public profile by handle

security: []

responses:

'200':

description: Public profile

content:

application/json:

schema:

$ref: '#/components/schemas/ProfileResponse'

'404':

$ref: '#/components/responses/NotFound'

/api/v1/me/profile:

patch:

tags: [Profiles]

operationId: updateMyProfile

summary: Update the authenticated owner's profile

requestBody:

required: true

content:

application/json:

schema:

$ref: '#/components/schemas/ProfileUpdate'

responses:

'200':

description: Updated profile

content:

application/json:

schema:

$ref: '#/components/schemas/ProfileResponse'

'401':

$ref: '#/components/responses/Unauthorized'

'422':

$ref: '#/components/responses/ValidationError'

/api/v1/posts:

get:

tags: [Content]

operationId: listPosts

summary: List visible posts

security: []

parameters:

- $ref: '#/components/parameters/Cursor'

- $ref: '#/components/parameters/Limit'

- $ref: '#/components/parameters/SourceTypeFilter'

- name: author_handle

in: query

schema:

type: string

responses:

'200':

description: Paginated posts

content:

application/json:

schema:

$ref: '#/components/schemas/PostCollectionResponse'

post:

tags: [Content]

operationId: createPost

summary: Create a local post

requestBody:

required: true

content:

application/json:

schema:

$ref: '#/components/schemas/PostWrite'

responses:

'201':

description: Post created

content:

application/json:

schema:

$ref: '#/components/schemas/PostResponse'

'401':

$ref: '#/components/responses/Unauthorized'

'422':

$ref: '#/components/responses/ValidationError'

/api/v1/posts/{postId}:

parameters:

- $ref: '#/components/parameters/PostId'

get:

tags: [Content]

operationId: getPost

summary: Get a visible post

security: []

responses:

'200':

description: Post details

content:

application/json:

schema:

$ref: '#/components/schemas/PostResponse'

'404':

$ref: '#/components/responses/NotFound'

patch:

tags: [Content]

operationId: updatePost

summary: Update an owned local post

requestBody:

required: true

content:

application/json:

schema:

$ref: '#/components/schemas/PostPatch'

responses:

'200':

description: Updated post

content:

application/json:

schema:

$ref: '#/components/schemas/PostResponse'

'403':

$ref: '#/components/responses/Forbidden'

'404':

$ref: '#/components/responses/NotFound'

'422':

$ref: '#/components/responses/ValidationError'

delete:

tags: [Content]

operationId: deletePost

summary: Soft-delete an owned local post

responses:

'204':

description: Post deleted

'403':

$ref: '#/components/responses/Forbidden'

'404':

$ref: '#/components/responses/NotFound'

/api/v1/timeline:

get:

tags: [Content]

operationId: getTimeline

summary: Get normalized local, external, and federated content

security:

- {}

- bearerAuth: []

parameters:

- $ref: '#/components/parameters/Cursor'

- $ref: '#/components/parameters/Limit'

- $ref: '#/components/parameters/SourceTypeFilter'

responses:

'200':

description: Paginated timeline

content:

application/json:

schema:

$ref: '#/components/schemas/PostCollectionResponse'

/api/v1/external-sources:

get:

tags: [External Sources]

operationId: listExternalSources

summary: List the owner's external sources and sync health

responses:

'200':

description: External sources

content:

application/json:

schema:

$ref: '#/components/schemas/ExternalSourceCollectionResponse'

'401':

$ref: '#/components/responses/Unauthorized'

post:

tags: [External Sources]

operationId: createExternalSource

summary: Save and schedule an external source

requestBody:

required: true

content:

application/json:

schema:

$ref: '#/components/schemas/ExternalSourceWrite'

responses:

'201':

description: Source connected

content:

application/json:

schema:

$ref: '#/components/schemas/ExternalSourceResponse'

'401':

$ref: '#/components/responses/Unauthorized'

'422':

$ref: '#/components/responses/ValidationError'

'502':

$ref: '#/components/responses/UpstreamError'

/api/v1/external-sources/test:

post:

tags: [External Sources]

operationId: testExternalSource

summary: Validate and preview an external source without saving it

requestBody:

required: true

content:

application/json:

schema:

$ref: '#/components/schemas/ExternalSourceTestRequest'

responses:

'200':

description: Source is reachable and preview was normalized

content:

application/json:

schema:

$ref: '#/components/schemas/ExternalSourceTestResponse'

'401':

$ref: '#/components/responses/Unauthorized'

'422':

$ref: '#/components/responses/ValidationError'

'502':

$ref: '#/components/responses/UpstreamError'

/api/v1/external-sources/{sourceId}:

parameters:

- $ref: '#/components/parameters/SourceId'

get:

tags: [External Sources]

operationId: getExternalSource

summary: Get source configuration and sync health

responses:

'200':

description: External source

content:

application/json:

schema:

$ref: '#/components/schemas/ExternalSourceResponse'

'404':

$ref: '#/components/responses/NotFound'

patch:

tags: [External Sources]

operationId: updateExternalSource

summary: Update source interval, visibility, or enabled state

requestBody:

required: true

content:

application/json:

schema:

$ref: '#/components/schemas/ExternalSourceUpdate'

responses:

'200':

description: Updated source

content:

application/json:

schema:

$ref: '#/components/schemas/ExternalSourceResponse'

'404':

$ref: '#/components/responses/NotFound'

'422':

$ref: '#/components/responses/ValidationError'

delete:

tags: [External Sources]

operationId: disconnectExternalSource

summary: Disconnect an external source

parameters:

- name: purge_imported_content

in: query

description: Remove previously imported posts when true

schema:

type: boolean

default: false

responses:

'204':

description: Source disconnected

'404':

$ref: '#/components/responses/NotFound'

/api/v1/external-sources/{sourceId}/sync:

parameters:

- $ref: '#/components/parameters/SourceId'

post:

tags: [External Sources]

operationId: syncExternalSource

summary: Queue an on-demand source synchronization

responses:

'202':

description: Synchronization queued

content:

application/json:

schema:

$ref: '#/components/schemas/JobResponse'

'404':

$ref: '#/components/responses/NotFound'

'409':

$ref: '#/components/responses/Conflict'

/api/v1/products:

get:

tags: [Commerce]

operationId: listProducts

summary: List available products

security: []

parameters:

- $ref: '#/components/parameters/Cursor'

- $ref: '#/components/parameters/Limit'

responses:

'200':

description: Products

content:

application/json:

schema:

$ref: '#/components/schemas/ProductCollectionResponse'

post:

tags: [Commerce]

operationId: createProduct

summary: Create a local product

requestBody:

required: true

content:

application/json:

schema:

$ref: '#/components/schemas/ProductWrite'

responses:

'201':

description: Product created

content:

application/json:

schema:

$ref: '#/components/schemas/ProductResponse'

'422':

$ref: '#/components/responses/ValidationError'

/api/v1/products/{productId}:

parameters:

- $ref: '#/components/parameters/ProductId'

get:

tags: [Commerce]

operationId: getProduct

summary: Get product details

security: []

responses:

'200':

description: Product details

content:

application/json:

schema:

$ref: '#/components/schemas/ProductResponse'

'404':

$ref: '#/components/responses/NotFound'

patch:

tags: [Commerce]

operationId: updateProduct

summary: Update an owned product

requestBody:

required: true

content:

application/json:

schema:

$ref: '#/components/schemas/ProductWrite'

responses:

'200':

description: Updated product

content:

application/json:

schema:

$ref: '#/components/schemas/ProductResponse'

'403':

$ref: '#/components/responses/Forbidden'

'404':

$ref: '#/components/responses/NotFound'

/api/v1/orders:

post:

tags: [Commerce]

operationId: createOrder

summary: Create an order with an immutable price snapshot

security:

- {}

- bearerAuth: []

requestBody:

required: true

content:

application/json:

schema:

$ref: '#/components/schemas/OrderCreate'

responses:

'201':

description: Order created

content:

application/json:

schema:

$ref: '#/components/schemas/OrderResponse'

'409':

$ref: '#/components/responses/Conflict'

'422':

$ref: '#/components/responses/ValidationError'

/api/v1/orders/{orderId}:

parameters:

- $ref: '#/components/parameters/OrderId'

- $ref: '#/components/parameters/OrderToken'

get:

tags: [Commerce]

operationId: getOrder

summary: Get an order

security:

- bearerAuth: []

- orderToken: []

responses:

'200':

description: Order details

content:

application/json:

schema:

$ref: '#/components/schemas/OrderResponse'

'401':

$ref: '#/components/responses/Unauthorized'

'404':

$ref: '#/components/responses/NotFound'

/api/v1/orders/{orderId}/payments:

parameters:

- $ref: '#/components/parameters/OrderId'

- $ref: '#/components/parameters/OrderToken'

- $ref: '#/components/parameters/IdempotencyKey'

post:

tags: [Payments]

operationId: createPayment

summary: Create an idempotent payment attempt

security:

- bearerAuth: []

- orderToken: []

requestBody:

required: true

content:

application/json:

schema:

$ref: '#/components/schemas/PaymentCreate'

responses:

'201':

description: Payment attempt created

content:

application/json:

schema:

$ref: '#/components/schemas/PaymentResponse'

'409':

$ref: '#/components/responses/Conflict'

'422':

$ref: '#/components/responses/ValidationError'

'502':

$ref: '#/components/responses/UpstreamError'

/api/v1/payments/{paymentId}:

parameters:

- $ref: '#/components/parameters/PaymentId'

- $ref: '#/components/parameters/OrderToken'

get:

tags: [Payments]

operationId: getPayment

summary: Get normalized payment state

security:

- bearerAuth: []

- orderToken: []

responses:

'200':

description: Payment details

content:

application/json:

schema:

$ref: '#/components/schemas/PaymentResponse'

'404':

$ref: '#/components/responses/NotFound'

/api/v1/payments/{paymentId}/cancel:

parameters:

- $ref: '#/components/parameters/PaymentId'

- $ref: '#/components/parameters/OrderToken'

post:

tags: [Payments]

operationId: cancelPayment

summary: Cancel a pending payment

security:

- bearerAuth: []

- orderToken: []

responses:

'200':

description: Payment cancelled

content:

application/json:

schema:

$ref: '#/components/schemas/PaymentResponse'

'409':

$ref: '#/components/responses/Conflict'

'404':

$ref: '#/components/responses/NotFound'

/api/v1/payments/{paymentId}/refunds:

parameters:

- $ref: '#/components/parameters/PaymentId'

- $ref: '#/components/parameters/IdempotencyKey'

post:

tags: [Payments]

operationId: refundPayment

summary: Request a full or partial refund

x-required-role: ADMIN

requestBody:

required: true

content:

application/json:

schema:

$ref: '#/components/schemas/RefundCreate'

responses:

'202':

description: Refund accepted

content:

application/json:

schema:

$ref: '#/components/schemas/RefundResponse'

'403':

$ref: '#/components/responses/Forbidden'

'409':

$ref: '#/components/responses/Conflict'

'422':

$ref: '#/components/responses/ValidationError'

/api/v1/webhooks/payments/{gatewayCode}:

parameters:

- $ref: '#/components/parameters/GatewayCode'

post:

tags: [Payments]

operationId: receivePaymentWebhook

summary: Receive a signed payment-provider webhook

description: Signature headers are provider-specific. Valid duplicates return 200.

security: []

requestBody:

required: true

content:

application/json:

schema:

type: object

additionalProperties: true

responses:

'200':

description: Event verified and acknowledged

content:

application/json:

schema:

$ref: '#/components/schemas/WebhookAcknowledgement'

'401':

description: Signature verification failed

content:

application/json:

schema:

$ref: '#/components/schemas/ErrorResponse'

/api/v1/admin/payment-gateways:

get:

tags: [Administration]

operationId: listPaymentGateways

summary: List gateway capabilities and configuration state

x-required-role: ADMIN

responses:

'200':

description: Gateway definitions

content:

application/json:

schema:

$ref: '#/components/schemas/PaymentGatewayCollectionResponse'

'403':

$ref: '#/components/responses/Forbidden'

/api/v1/admin/payment-gateways/{gatewayCode}:

parameters:

- $ref: '#/components/parameters/GatewayCode'

put:

tags: [Administration]

operationId: configurePaymentGateway

summary: Configure and activate a payment gateway

x-required-role: ADMIN

requestBody:

required: true

content:

application/json:

schema:

$ref: '#/components/schemas/PaymentGatewayConfigWrite'

responses:

'200':

description: Gateway configured; secrets are masked

content:

application/json:

schema:

$ref: '#/components/schemas/PaymentGatewayResponse'

'403':

$ref: '#/components/responses/Forbidden'

'422':

$ref: '#/components/responses/ValidationError'

/api/v1/admin/integration-jobs:

get:

tags: [Administration]

operationId: listIntegrationJobs

summary: Inspect external integration jobs

x-required-role: ADMIN

parameters:

- $ref: '#/components/parameters/Cursor'

- $ref: '#/components/parameters/Limit'

- name: status

in: query

schema:

$ref: '#/components/schemas/JobStatus'

responses:

'200':

description: Integration jobs

content:

application/json:

schema:

$ref: '#/components/schemas/JobCollectionResponse'

'403':

$ref: '#/components/responses/Forbidden'

/api/v1/admin/integration-jobs/{jobId}/retry:

parameters:

- $ref: '#/components/parameters/JobId'

post:

tags: [Administration]

operationId: retryIntegrationJob

summary: Retry a failed integration job

x-required-role: ADMIN

responses:

'202':

description: Retry queued

content:

application/json:

schema:

$ref: '#/components/schemas/JobResponse'

'403':

$ref: '#/components/responses/Forbidden'

'409':

$ref: '#/components/responses/Conflict'

/.well-known/fpdp:

get:

tags: [Federation]

operationId: discoverNode

summary: Discover the node identity and capabilities

security: []

responses:

'200':

description: Node discovery document

content:

application/json:

schema:

$ref: '#/components/schemas/NodeDiscovery'

components:

securitySchemes:

bearerAuth:

type: http

scheme: bearer

bearerFormat: opaque

orderToken:

type: apiKey

in: header

name: X-Order-Token

parameters:

Cursor:

name: cursor

in: query

schema:

type: string

maxLength: 512

Limit:

name: limit

in: query

schema:

type: integer

minimum: 1

maximum: 100

default: 20

Handle:

name: handle

in: path

required: true

schema:

type: string

pattern: '^[a-z0-9][a-z0-9-]{2,62}$'

PostId:

name: postId

in: path

required: true

schema: { $ref: '#/components/schemas/Uuid' }

SourceId:

name: sourceId

in: path

required: true

schema: { $ref: '#/components/schemas/Uuid' }

ProductId:

name: productId

in: path

required: true

schema: { $ref: '#/components/schemas/Uuid' }

OrderId:

name: orderId

in: path

required: true

schema: { $ref: '#/components/schemas/Uuid' }

PaymentId:

name: paymentId

in: path

required: true

schema: { $ref: '#/components/schemas/Uuid' }

JobId:

name: jobId

in: path

required: true

schema: { $ref: '#/components/schemas/Uuid' }

GatewayCode:

name: gatewayCode

in: path

required: true

schema:

type: string

pattern: '^[A-Z][A-Z0-9_]{1,63}$'

examples: [DUMMY, MIDTRANS]

OrderToken:

name: X-Order-Token

in: header

required: false

description: Guest capability token; do not place it in URLs or logs.

schema:

type: string

minLength: 32

IdempotencyKey:

name: Idempotency-Key

in: header

required: true

schema:

type: string

minLength: 16

maxLength: 128

SourceTypeFilter:

name: source_type

in: query

schema:

type: array

items:

$ref: '#/components/schemas/SourceType'

style: form

explode: false

responses:

Unauthorized:

description: Authentication is missing or invalid

content:

application/json:

schema: { $ref: '#/components/schemas/ErrorResponse' }

Forbidden:

description: Authenticated principal is not allowed to perform this operation

content:

application/json:

schema: { $ref: '#/components/schemas/ErrorResponse' }

NotFound:

description: Resource does not exist or is not visible to the caller

content:

application/json:

schema: { $ref: '#/components/schemas/ErrorResponse' }

Conflict:

description: Resource state or idempotency conflict

content:

application/json:

schema: { $ref: '#/components/schemas/ErrorResponse' }

ValidationError:

description: Request failed semantic validation

content:

application/json:

schema: { $ref: '#/components/schemas/ErrorResponse' }

UpstreamError:

description: An external provider failed or returned an invalid response

content:

application/json:

schema: { $ref: '#/components/schemas/ErrorResponse' }

ServiceUnavailable:

description: Application or a critical dependency is unavailable

content:

application/json:

schema: { $ref: '#/components/schemas/ErrorResponse' }

schemas:

Uuid:

type: string

format: uuid

Timestamp:

type: string

format: date-time

Uri:

type: string

format: uri

maxLength: 2048

RequestMeta:

type: object

required: [request_id]

properties:

request_id:

type: string

PaginationMeta:

allOf:

- $ref: '#/components/schemas/RequestMeta'

- type: object

required: [has_more]

properties:

next_cursor:

type: [string, 'null']

has_more:

type: boolean

ErrorDetail:

type: object

properties:

field:

type: string

reason:

type: string

Error:

type: object

required: [code, message, request_id]

properties:

code:

type: string

examples: [VALIDATION_ERROR]

message:

type: string

details:

type: array

items: { $ref: '#/components/schemas/ErrorDetail' }

request_id:

type: string

ErrorResponse:

type: object

required: [error]

properties:

error: { $ref: '#/components/schemas/Error' }

HealthResponse:

type: object

required: [data, meta]

properties:

data:

type: object

required: [status, version, dependencies]

properties:

status:

type: string

enum: [OK, DEGRADED]

version:

type: string

dependencies:

type: object

additionalProperties:

type: string

enum: [UP, DOWN, UNKNOWN]

meta: { $ref: '#/components/schemas/RequestMeta' }

RegisterRequest:

type: object

additionalProperties: false

required: [email, password, handle, display_name]

properties:

email:

type: string

format: email

maxLength: 254

password:

type: string

format: password

minLength: 12

maxLength: 128

handle:

type: string

pattern: '^[a-z0-9][a-z0-9-]{2,62}$'

display_name:

type: string

minLength: 1

maxLength: 128

locale:

type: string

enum: [en, id]

default: id

LoginRequest:

type: object

additionalProperties: false

required: [email, password]

properties:

email:

type: string

format: email

password:

type: string

format: password

User:

type: object

required: [id, email, role, created_at]

properties:

id: { $ref: '#/components/schemas/Uuid' }

email:

type: string

format: email

role:

type: string

enum: [OWNER, ADMIN]

created_at: { $ref: '#/components/schemas/Timestamp' }

Node:

type: object

required: [id, domain, status]

properties:

id: { $ref: '#/components/schemas/Uuid' }

domain:

type: string

format: hostname

status:

type: string

enum: [PROVISIONING, ACTIVE, SUSPENDED]

Token:

type: object

required: [access_token, token_type, expires_in]

properties:

access_token:

type: string

token_type:

type: string

const: Bearer

expires_in:

type: integer

minimum: 1

AuthData:

type: object

required: [user, node, token]

properties:

user: { $ref: '#/components/schemas/User' }

node: { $ref: '#/components/schemas/Node' }

token: { $ref: '#/components/schemas/Token' }

AuthResponse:

type: object

required: [data, meta]

properties:

data: { $ref: '#/components/schemas/AuthData' }

meta: { $ref: '#/components/schemas/RequestMeta' }

MeData:

type: object

required: [user, node, profile]

properties:

user: { $ref: '#/components/schemas/User' }

node: { $ref: '#/components/schemas/Node' }

profile: { $ref: '#/components/schemas/Profile' }

MeResponse:

type: object

required: [data, meta]

properties:

data: { $ref: '#/components/schemas/MeData' }

meta: { $ref: '#/components/schemas/RequestMeta' }

Visibility:

type: string

enum: [PUBLIC, UNLISTED, PRIVATE]

Profile:

type: object

required: [id, handle, display_name, visibility, canonical_url]

properties:

id: { $ref: '#/components/schemas/Uuid' }

handle:

type: string

display_name:

type: string

bio:

type: [string, 'null']

maxLength: 2000

avatar_url:

oneOf:

- $ref: '#/components/schemas/Uri'

- type: 'null'

links:

type: array

maxItems: 20

items:

type: object

required: [label, url]

properties:

label:

type: string

maxLength: 64

url: { $ref: '#/components/schemas/Uri' }

visibility: { $ref: '#/components/schemas/Visibility' }

canonical_url: { $ref: '#/components/schemas/Uri' }

ProfileUpdate:

type: object

additionalProperties: false

minProperties: 1

properties:

display_name:

type: string

minLength: 1

maxLength: 128

bio:

type: [string, 'null']

maxLength: 2000

avatar_url:

oneOf:

- $ref: '#/components/schemas/Uri'

- type: 'null'

visibility: { $ref: '#/components/schemas/Visibility' }

ProfileResponse:

type: object

required: [data, meta]

properties:

data: { $ref: '#/components/schemas/Profile' }

meta: { $ref: '#/components/schemas/RequestMeta' }

SourceType:

type: string

enum: [LOCAL, EXTERNAL, FEDERATED]

PostType:

type: string

enum: [NOTE, ARTICLE, MEDIA]

Author:

type: object

required: [display_name]

properties:

handle:

type: [string, 'null']

display_name:

type: string

profile_url:

oneOf:

- $ref: '#/components/schemas/Uri'

- type: 'null'

avatar_url:

oneOf:

- $ref: '#/components/schemas/Uri'

- type: 'null'

Media:

type: object

additionalProperties: false

required: [type, url]

properties:

type:

type: string

enum: [IMAGE, VIDEO, AUDIO, FILE]

url:

type: string

format: uri

maxLength: 2048

pattern: '^https://'

description: HTTPS URL without embedded user credentials.

alt_text:

type: [string, 'null']

maxLength: 500

Post:

type: object

required:

- id

- post_type

- source_type

- source_provider

- canonical_url

- author

- visibility

- published_at

properties:

id: { $ref: '#/components/schemas/Uuid' }

title:

type: [string, 'null']

maxLength: 255

content:

type: string

post_type: { $ref: '#/components/schemas/PostType' }

source_type: { $ref: '#/components/schemas/SourceType' }

source_provider:

type: string

examples: [FPDP, RSS, ATOM, CUSTOM_API]

canonical_url: { $ref: '#/components/schemas/Uri' }

author: { $ref: '#/components/schemas/Author' }

media:

type: array

items: { $ref: '#/components/schemas/Media' }

visibility: { $ref: '#/components/schemas/Visibility' }

published_at: { $ref: '#/components/schemas/Timestamp' }

updated_at: { $ref: '#/components/schemas/Timestamp' }

PostWrite:

type: object

additionalProperties: false

required: [content, post_type, visibility]

properties:

title:

type: [string, 'null']

maxLength: 255

content:

type: string

minLength: 1

maxLength: 100000

post_type: { $ref: '#/components/schemas/PostType' }

visibility: { $ref: '#/components/schemas/Visibility' }

published_at:

oneOf:

- $ref: '#/components/schemas/Timestamp'

- type: 'null'

media:

type: array

maxItems: 10

items: { $ref: '#/components/schemas/Media' }

PostPatch:

type: object

additionalProperties: false

minProperties: 1

properties:

title:

type: [string, 'null']

maxLength: 255

content:

type: string

minLength: 1

maxLength: 100000

post_type: { $ref: '#/components/schemas/PostType' }

visibility: { $ref: '#/components/schemas/Visibility' }

published_at:

description: Set to null to return the post to draft status.

oneOf:

- $ref: '#/components/schemas/Timestamp'

- type: 'null'

media:

description: Replaces the complete ordered media list when provided.

type: array

maxItems: 10

items: { $ref: '#/components/schemas/Media' }

PostResponse:

type: object

required: [data, meta]

properties:

data: { $ref: '#/components/schemas/Post' }

meta: { $ref: '#/components/schemas/RequestMeta' }

PostCollectionResponse:

type: object

required: [data, meta]

properties:

data:

type: array

items: { $ref: '#/components/schemas/Post' }

meta: { $ref: '#/components/schemas/PaginationMeta' }

ExternalProvider:

type: string

enum: [RSS, ATOM, CUSTOM_API]

ExternalSourceStatus:

type: string

enum: [PENDING, ACTIVE, ERROR, DISCONNECTED]

ExternalSource:

type: object

required: [id, provider, source_url, sync_enabled, sync_interval, status]

properties:

id: { $ref: '#/components/schemas/Uuid' }

provider: { $ref: '#/components/schemas/ExternalProvider' }

source_url: { $ref: '#/components/schemas/Uri' }

display_name:

type: [string, 'null']

sync_enabled:

type: boolean

sync_interval:

type: integer

minimum: 300

maximum: 604800

description: Interval in seconds

default_visibility: { $ref: '#/components/schemas/Visibility' }

status: { $ref: '#/components/schemas/ExternalSourceStatus' }

last_sync_at:

oneOf:

- $ref: '#/components/schemas/Timestamp'

- type: 'null'

next_sync_at:

oneOf:

- $ref: '#/components/schemas/Timestamp'

- type: 'null'

last_error:

type: [string, 'null']

description: Sanitized error; credentials must never be included.

ExternalSourceWrite:

type: object

additionalProperties: false

required: [provider, source_url]

properties:

provider: { $ref: '#/components/schemas/ExternalProvider' }

source_url: { $ref: '#/components/schemas/Uri' }

sync_enabled:

type: boolean

default: true

sync_interval:

type: integer

minimum: 300

maximum: 604800

default: 3600

default_visibility:

$ref: '#/components/schemas/Visibility'

credentials:

type: object

additionalProperties:

type: string

writeOnly: true

description: Encrypted at rest and never returned.

ExternalSourceUpdate:

type: object

additionalProperties: false

minProperties: 1

properties:

sync_enabled:

type: boolean

sync_interval:

type: integer

minimum: 300

maximum: 604800

default_visibility: { $ref: '#/components/schemas/Visibility' }

ExternalSourceTestRequest:

allOf:

- $ref: '#/components/schemas/ExternalSourceWrite'

ExternalSourceTestData:

type: object

required: [valid, provider, preview]

properties:

valid:

type: boolean

provider: { $ref: '#/components/schemas/ExternalProvider' }

display_name:

type: [string, 'null']

preview:

type: array

maxItems: 5

items: { $ref: '#/components/schemas/Post' }

warnings:

type: array

items:

type: string

ExternalSourceTestResponse:

type: object

required: [data, meta]

properties:

data: { $ref: '#/components/schemas/ExternalSourceTestData' }

meta: { $ref: '#/components/schemas/RequestMeta' }

ExternalSourceResponse:

type: object

required: [data, meta]

properties:

data: { $ref: '#/components/schemas/ExternalSource' }

meta: { $ref: '#/components/schemas/RequestMeta' }

ExternalSourceCollectionResponse:

type: object

required: [data, meta]

properties:

data:

type: array

items: { $ref: '#/components/schemas/ExternalSource' }

meta: { $ref: '#/components/schemas/RequestMeta' }

Money:

type: object

required: [amount, currency]

properties:

amount:

type: string

pattern: '^(0|[1-9][0-9]*)(\.[0-9]{1,2})?$'

examples: ['250000.00']

currency:

type: string

pattern: '^[A-Z]{3}$'

examples: [IDR]

ProductStatus:

type: string

enum: [DRAFT, ACTIVE, INACTIVE]

Product:

type: object

required: [id, name, price, status, source_type, canonical_url]

properties:

id: { $ref: '#/components/schemas/Uuid' }

name:

type: string

description:

type: [string, 'null']

price: { $ref: '#/components/schemas/Money' }

stock:

type: [integer, 'null']

minimum: 0

status: { $ref: '#/components/schemas/ProductStatus' }

source_type: { $ref: '#/components/schemas/SourceType' }

canonical_url: { $ref: '#/components/schemas/Uri' }

ProductWrite:

type: object

additionalProperties: false

required: [name, price, status]

properties:

name:

type: string

minLength: 1

maxLength: 255

description:

type: [string, 'null']

maxLength: 10000

price: { $ref: '#/components/schemas/Money' }

stock:

type: [integer, 'null']

minimum: 0

status: { $ref: '#/components/schemas/ProductStatus' }

ProductResponse:

type: object

required: [data, meta]

properties:

data: { $ref: '#/components/schemas/Product' }

meta: { $ref: '#/components/schemas/RequestMeta' }

ProductCollectionResponse:

type: object

required: [data, meta]

properties:

data:

type: array

items: { $ref: '#/components/schemas/Product' }

meta: { $ref: '#/components/schemas/PaginationMeta' }

OrderStatus:

type: string

enum: [CREATED, AWAITING_PAYMENT, PAID, CANCELLED, FULFILLED, REFUNDED]

OrderLineCreate:

type: object

additionalProperties: false

required: [product_id, quantity]

properties:

product_id: { $ref: '#/components/schemas/Uuid' }

quantity:

type: integer

minimum: 1

maximum: 999

Customer:

type: object

required: [email, name]

properties:

email:

type: string

format: email

name:

type: string

maxLength: 128

OrderCreate:

type: object

additionalProperties: false

required: [items, customer]

properties:

items:

type: array

minItems: 1

maxItems: 100

items: { $ref: '#/components/schemas/OrderLineCreate' }

customer: { $ref: '#/components/schemas/Customer' }

OrderLine:

type: object

required: [product_id, name, quantity, unit_price, subtotal]

properties:

product_id: { $ref: '#/components/schemas/Uuid' }

name:

type: string

quantity:

type: integer

unit_price: { $ref: '#/components/schemas/Money' }

subtotal: { $ref: '#/components/schemas/Money' }

Order:

type: object

required: [id, status, items, total, customer, created_at]

properties:

id: { $ref: '#/components/schemas/Uuid' }

status: { $ref: '#/components/schemas/OrderStatus' }

items:

type: array

items: { $ref: '#/components/schemas/OrderLine' }

total: { $ref: '#/components/schemas/Money' }

customer: { $ref: '#/components/schemas/Customer' }

created_at: { $ref: '#/components/schemas/Timestamp' }

order_token:

type: string

writeOnly: true

description: Returned only at guest order creation.

OrderResponse:

type: object

required: [data, meta]

properties:

data: { $ref: '#/components/schemas/Order' }

meta: { $ref: '#/components/schemas/RequestMeta' }

PaymentStatus:

type: string

enum: [PENDING, PAID, FAILED, EXPIRED, CANCELLED, PARTIALLY_REFUNDED, REFUNDED]

PaymentCreate:

type: object

additionalProperties: false

properties:

gateway_code:

type: string

pattern: '^[A-Z][A-Z0-9_]{1,63}$'

payment_method:

type: [string, 'null']

maxLength: 64

return_url:

oneOf:

- $ref: '#/components/schemas/Uri'

- type: 'null'

Payment:

type: object

required: [id, order_id, gateway_code, amount, status, created_at]

properties:

id: { $ref: '#/components/schemas/Uuid' }

order_id: { $ref: '#/components/schemas/Uuid' }

gateway_code:

type: string

external_transaction_id:

type: [string, 'null']

payment_method:

type: [string, 'null']

amount: { $ref: '#/components/schemas/Money' }

fee: { $ref: '#/components/schemas/Money' }

status: { $ref: '#/components/schemas/PaymentStatus' }

payment_url:

oneOf:

- $ref: '#/components/schemas/Uri'

- type: 'null'

expires_at:

oneOf:

- $ref: '#/components/schemas/Timestamp'

- type: 'null'

paid_at:

oneOf:

- $ref: '#/components/schemas/Timestamp'

- type: 'null'

created_at: { $ref: '#/components/schemas/Timestamp' }

PaymentResponse:

type: object

required: [data, meta]

properties:

data: { $ref: '#/components/schemas/Payment' }

meta: { $ref: '#/components/schemas/RequestMeta' }

RefundCreate:

type: object

additionalProperties: false

required: [amount, reason]

properties:

amount: { $ref: '#/components/schemas/Money' }

reason:

type: string

minLength: 1

maxLength: 500

Refund:

type: object

required: [id, payment_id, amount, status, created_at]

properties:

id: { $ref: '#/components/schemas/Uuid' }

payment_id: { $ref: '#/components/schemas/Uuid' }

amount: { $ref: '#/components/schemas/Money' }

status:

type: string

enum: [PENDING, SUCCEEDED, FAILED]

created_at: { $ref: '#/components/schemas/Timestamp' }

RefundResponse:

type: object

required: [data, meta]

properties:

data: { $ref: '#/components/schemas/Refund' }

meta: { $ref: '#/components/schemas/RequestMeta' }

WebhookAcknowledgement:

type: object

required: [received, duplicate]

properties:

received:

type: boolean

const: true

duplicate:

type: boolean

PaymentGateway:

type: object

required: [code, name, status, environment, capabilities, configured]

properties:

code:

type: string

name:

type: string

status:

type: string

enum: [ACTIVE, INACTIVE]

environment:

type: string

enum: [SANDBOX, PRODUCTION]

capabilities:

type: array

items:

type: string

enum: [REFUND, RECURRING, QRIS, VA, CREDIT_CARD, EWALLET]

configured:

type: boolean

masked_configuration:

type: object

additionalProperties:

type: string

readOnly: true

PaymentGatewayConfigWrite:

type: object

additionalProperties: false

required: [status, environment, configuration]

properties:

status:

type: string

enum: [ACTIVE, INACTIVE]

environment:

type: string

enum: [SANDBOX, PRODUCTION]

configuration:

type: object

additionalProperties:

type: string

writeOnly: true

PaymentGatewayResponse:

type: object

required: [data, meta]

properties:

data: { $ref: '#/components/schemas/PaymentGateway' }

meta: { $ref: '#/components/schemas/RequestMeta' }

PaymentGatewayCollectionResponse:

type: object

required: [data, meta]

properties:

data:

type: array

items: { $ref: '#/components/schemas/PaymentGateway' }

meta: { $ref: '#/components/schemas/RequestMeta' }

JobStatus:

type: string

enum: [QUEUED, RUNNING, SUCCEEDED, FAILED, RETRY_SCHEDULED]

IntegrationJob:

type: object

required: [id, provider, job_type, status, retry_count, created_at]

properties:

id: { $ref: '#/components/schemas/Uuid' }

provider:

type: string

job_type:

type: string

status: { $ref: '#/components/schemas/JobStatus' }

retry_count:

type: integer

minimum: 0

next_retry_at:

oneOf:

- $ref: '#/components/schemas/Timestamp'

- type: 'null'

last_error:

type: [string, 'null']

description: Sanitized; must not contain credentials.

created_at: { $ref: '#/components/schemas/Timestamp' }

JobResponse:

type: object

required: [data, meta]

properties:

data: { $ref: '#/components/schemas/IntegrationJob' }

meta: { $ref: '#/components/schemas/RequestMeta' }

JobCollectionResponse:

type: object

required: [data, meta]

properties:

data:

type: array

items: { $ref: '#/components/schemas/IntegrationJob' }

meta: { $ref: '#/components/schemas/PaginationMeta' }

NodeDiscovery:

type: object

required: [node_id, domain, protocol_version, api_base_url, capabilities]

properties:

node_id: { $ref: '#/components/schemas/Uuid' }

domain:

type: string

format: hostname

protocol_version:

type: string

api_base_url: { $ref: '#/components/schemas/Uri' }

capabilities:

type: array

items:

type: string

enum: [PROFILE, CONTENT, FEDERATION, PRODUCTS, ORDERS, PAYMENTS]

public_key:

type: [string, 'null']

security:

- bearerAuth: []