FPDP API Contract (English)

1. Scope and status

This document defines the intended HTTP contract for the FPDP MVP. It is a design contract; the current repository does not yet implement most routes. The machine-readable source is <code>openapi.yaml</code>.

Base path: /api/v1

Authentication:

- public reads do not require authentication;

- owner/admin operations use Authorization: Bearer <token>;

- payment webhooks use provider-specific signatures and must not use user bearer tokens.

Content types: application/json, except webhook bodies which may be provider-specific JSON.

2. Common conventions

- IDs exposed by the API are UUID strings.

- Timestamps use RFC 3339 UTC values.

- Money is represented as a decimal string plus an ISO 4217 currency code to prevent floating-point errors.

- Collection responses use cursor pagination: data, meta.next_cursor, and meta.has_more.

- Mutable resources return an ETag when optimistic concurrency is supported.

- All content records identify source_type, source_provider, and canonical_url.

- Retrying a create-payment request requires the same Idempotency-Key header.

Standard success envelope:

{"data": {}, "meta": {"request_id": "req_..."}}

Standard error envelope:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request is invalid.",
    "details": [{"field": "email", "reason": "invalid_format"}],
    "request_id": "req_..."
  }
}

3. Endpoint list

System and authentication

| Method | Path | Auth | Purpose |

|---|---|---|---|

| GET | /health | Public | Liveness and dependency summary |

| POST | /auth/register | Public | Register an owner and initial node |

| POST | /auth/login | Public | Exchange credentials for access token. With 2FA on: {mfa_required: true, mfa_token, expires_in: 300} instead, and no token |

| POST | /auth/login/verify | Public | {mfa_token, code} (6-digit code or recovery code) → same response as a login without 2FA. 5 wrong codes end the challenge (401) |

| POST | /auth/logout | Bearer | Revoke the current token |

| GET | /me | Bearer | Return authenticated user and node context |

| GET | /me/sessions | Bearer | Devices signed in (id, device, truncated IP, created/last used, current) |

| DELETE | /me/sessions/{id} | Bearer | Log one other device out (the current one: use logout) |

| POST | /me/sessions/revoke-others | Bearer | Log every other device out; returns revoked |

| POST | /me/password | Bearer | {current_password, new_password}; logs every other device out |

| GET | /me/2fa | Bearer | {enabled, enabled_at, recovery_codes_remaining} |

| POST | /me/2fa/setup | Bearer | New pending secret: {secret, otpauth_uri} for the QR code (409 if already on) |

| POST | /me/2fa/confirm | Bearer | {code} → turns 2FA on; {recovery_codes[10], other_sessions_revoked}, shown only here |

| POST | /me/2fa/disable | Bearer | {password, code} (code or recovery code) → status |

| POST | /me/2fa/recovery-codes | Bearer | {code} (app code only) → {recovery_codes[10]}; the old set stops working |

Profiles and content

| Method | Path | Auth | Purpose |

|---|---|---|---|

| GET | /profiles/{handle} | Public | Read a public profile |

| PATCH | /me/profile | Bearer | Update the owner's profile |

| GET | /posts | Public | List posts with source/visibility filters |

| POST | /posts | Bearer | Create a local post |

| GET | /posts/{postId} | Public | Read a visible post |

| PATCH | /posts/{postId} | Bearer | Update an owned local post |

| DELETE | /posts/{postId} | Bearer | Soft-delete an owned local post |

Post writes accept up to 10 ordered media metadata items (IMAGE, VIDEO, AUDIO, or FILE). Media URLs must be absolute HTTPS URLs without embedded credentials; alternative text is limited to 500 characters. Supplying media in a PATCH replaces the complete media list atomically.

| GET | /timeline | Public/optional Bearer | Return normalized local, external, and federated content |

External sources

| Method | Path | Auth | Purpose |

|---|---|---|---|

| GET | /external-sources | Bearer | List owner connections and sync health |

| POST | /external-sources/test | Bearer | Validate and preview a source without saving it |

| POST | /external-sources | Bearer | Save and schedule a source |

| GET | /external-sources/{sourceId} | Bearer | Read source configuration and health |

| PATCH | /external-sources/{sourceId} | Bearer | Change interval, visibility, or enabled state |

| DELETE | /external-sources/{sourceId} | Bearer | Disconnect a source |

| POST | /external-sources/{sourceId}/sync | Bearer | Queue an on-demand synchronization |

Products, orders, and payments

| Method | Path | Auth | Purpose |

|---|---|---|---|

| GET | /products | Public | List available products |

| POST | /products | Bearer | Create a local product |

| GET | /products/{productId} | Public | Read product details |

| PATCH | /products/{productId} | Bearer | Update an owned product |

| GET | /products/{productId}/download | Visitor Bearer (a paid order) | Download list: digital_asset_url and files, each with a download_url valid 15 minutes |

| GET | /products/{productId}/digital-assets/{kind}/file | ?visitor=&expires=&token= (15 minutes) | The file itself, for a plain link; the purchase is re-checked |

| POST | /orders | Public/optional Bearer | Create an order and immutable total snapshot |

| GET | /orders/{orderId} | Bearer/order token | Read an order |

| POST | /orders/{orderId}/payments | Bearer/order token | Create a payment attempt; requires Idempotency-Key |

| GET | /payments/{paymentId} | Bearer/order token | Read normalized payment state |

| POST | /payments/{paymentId}/cancel | Bearer/order token | Cancel a pending payment |

| POST | /payments/{paymentId}/refunds | Admin Bearer | Request full or partial refund |

| POST | /webhooks/payments/{gatewayCode} | Signature | Receive and normalize provider events |

Owner payment management is implemented today (owner Bearer) at these paths:

| Method | Path | Purpose |

|---|---|---|

| GET | /me/payments/pending | List PENDING payments for manual confirmation |

| POST | /me/payments/{uuid}/confirm | Mark a PENDING payment PAID and run fulfillment |

| POST | /me/payments/{uuid}/cancel | Cancel a PENDING payment at the gateway (when it has an API) and locally; closes the related order |

| POST | /me/payments/{uuid}/refund | Body {"amount"?: number, "manual"?: bool}; no amount = full refund; manual: true records a refund made outside the gateway; a full refund undoes fulfillment |

| POST | /me/payments/reconcile | Check old PENDING payments with the provider, and cancelled payments that were paid anyway |

| POST | /payments/webhook/{gateway} | Provider webhook (replaces /webhooks/payments/{gatewayCode} above) |

Administration and federation

| Method | Path | Auth | Purpose |

|---|---|---|---|

| GET | /admin/payment-gateways | Admin Bearer | List gateway capabilities and configuration status |

| PUT | /admin/payment-gateways/{gatewayCode} | Admin Bearer | Configure and activate a gateway |

| GET | /admin/integration-jobs | Admin Bearer | Inspect synchronization queue and failures |

| POST | /admin/integration-jobs/{jobId}/retry | Admin Bearer | Retry a failed job |

| GET | /.well-known/fpdp | Public | Discover node identity and capabilities |

Node-to-node orders (FEDERATION-CONCEPT §11b)

| Method | Path | Auth | Purpose |

|---|---|---|---|

| POST | /federation/orders | HTTP Signature (buyer actor) | fpdp:OrderRequest → 201 {order} (200 for a retry with the same clientReference) |

| GET | /federation/orders/{orderId} | HTTP Signature (the actor that ordered) | Order status, items, and — once paid — download links |

| GET | /federation/orders/{orderId}/downloads/{productId}/{kind} | ?expires=&token= (15 minutes) | Digital file of a paid order |

| GET | /me/fediverse-shop | Bearer | Products from followed accounts; orderable when the seller takes node-to-node orders |

| GET | /me/purchases | Bearer | The owner's orders on other nodes |

| POST | /me/purchases | Bearer | {post_id, quantity, buyer_name, buyer_email, shipping_address, notes} → purchase with payment_url; 409 with the seller's reason if refused |

| POST | /me/purchases/{id}/refresh | Bearer | Re-read the status from the seller (or re-send an order that never got through) |

4. Important behavior

Source test and connection

POST /external-sources/test performs a bounded server-side fetch, blocks private/reserved network targets, applies timeouts and size limits, and returns normalized preview items. Saving a source is a separate explicit action.

Timeline provenance

Every item contains:

- source_type: LOCAL, EXTERNAL, or FEDERATED;

- source_provider: e.g. FPDP, RSS, ATOM, CUSTOM_API;

- canonical_url: authoritative original URL;

- author identity and publication timestamp.

Payment idempotency and webhooks

- A client reuses one Idempotency-Key for retries of the same logical payment request.

- The server returns the original result when the key and payload match.

- A reused key with a different payload returns 409 IDEMPOTENCY_CONFLICT.

- Webhooks are verified before processing and deduplicated using provider + event ID.

- A valid duplicate webhook returns 200 without applying the transition twice.

Suggested status codes

| Status | Meaning |

|---|---|

| 200 | Read/update success or accepted duplicate webhook |

| 201 | Resource created |

| 202 | Async synchronization/refund accepted |

| 204 | Logout or deletion completed |

| 400 | Malformed request |

| 401 | Missing/invalid authentication or webhook signature |

| 403 | Authenticated but not authorized |

| 404 | Resource unavailable or not visible |

| 409 | State or idempotency conflict |

| 422 | Semantic validation failure |

| 429 | Rate limit exceeded |

| 502 | Upstream provider failure |