Skip to content

API Conventions

The contract every Tomoda endpoint follows. New endpoints must adhere to these conventions; reviewers will reject PRs that diverge.

Content type

All non-binary request and response bodies are JSON. Set:

Content-Type: application/json
Accept: application/json

File uploads use multipart/form-data (avatar upload, chat image upload, group avatar). Webhooks (Stripe, Apple) use whatever the provider sends and we parse it directly.

Status codes

Code When
200 OK Successful GET / PATCH / DELETE / non-creating POST
201 Created POST that creates a new resource (must return the resource in the body)
204 No Content DELETE with nothing to return
400 Bad Request Input validation failure
401 Unauthorized Missing or invalid JWT / API key
403 Forbidden Authenticated but not authorised (admin-only route, suspended account, blocked IP)
404 Not Found Resource doesn't exist or isn't visible to this user
409 Conflict Idempotency / uniqueness violation (e.g. duplicate friend request)
422 Unprocessable Entity Semantic validation failure (input parses but business rules reject it)
429 Too Many Requests Rate limit hit
500 Internal Server Error Unhandled error — always logged with a trace ID

Error response shape

Every error response is a JSON object with at least an error or message field. The canonical shape we standardise on for new endpoints:

{
  "code": "AUTH_REQUIRED",
  "message": "You must be signed in to perform this action.",
  "details": {
    "field": "email",
    "reason": "format"
  }
}
  • code — a stable, machine-readable identifier. Clients branch on this.
  • message — a human-readable description, safe to surface to end users (already translated where applicable).
  • details — optional, free-form per-error data (e.g. validation errors keyed by field).

Legacy endpoints still return { "error": "..." } or { "error": "...", "reason": "..." }. Treat both shapes defensively on the client side until migration is complete.

Standard error codes

Code HTTP Meaning
AUTH_REQUIRED 401 No credentials or invalid signature
TOKEN_EXPIRED 401 JWT past its exp — call /auth/refresh
FORBIDDEN 403 Authenticated but not permitted
IP_BLOCKED 403 Caller's IP is on the block list (see Security → Rate Limiting)
NOT_FOUND 404 Resource missing or not visible
VALIDATION_ERROR 400 / 422 Input failed validation; details carries field-level errors
CONFLICT 409 Idempotency / unique constraint hit
RATE_LIMITED 429 Slow down — see Retry-After
INTERNAL_ERROR 500 Unhandled exception (trace ID is in the response and logs)

Add new codes sparingly. Reuse VALIDATION_ERROR for any input issue rather than minting one code per field.

Pagination

The backend uses offset / limit pagination on list endpoints. Query parameters:

Param Type Default Max
limit int 20 100
offset int 0

A list response looks like:

{
  "items": [ /* … */ ],
  "total": 142,
  "limit": 20,
  "offset": 40
}

Some endpoints (chat history, moments feed) use cursor pagination instead because they need stable ordering across writes. Those endpoints accept a cursor query param and return next_cursor in the response; check the Swagger spec for the per-endpoint contract.

Dates and times

  • All timestamps are ISO 8601 with a UTC offset: 2026-05-23T14:30:00Z. We never return naive timestamps without a zone.
  • Durations are returned as ISO 8601 durations (PT15M) or as integer seconds with a clear field name (expires_in_seconds).
  • Event start/end times are stored in UTC but the API returns the event's IANA timezone (Asia/Tokyo) so the client can render local time.

IDs

UUIDs are used for everything user-created. Format: lowercase canonical UUID (b3a4d2c1-…). Strings, not bytes. Some legacy entities use auto-incrementing integers — these are being phased out; do not introduce new ones.

Idempotency

Mutating endpoints aim to be idempotent on natural unique keys (e.g. friend requests by (from, to), OAuth login by external ID). Return 409 Conflict rather than creating duplicates.

For explicit client-driven idempotency (payment endpoints), pass an Idempotency-Key header; the server will short-circuit duplicate requests within a 24-hour window. Today this is only implemented on the Stripe payment endpoints — consult backend/internal/handlers/payment_handler.go for the exact behavior.

Trace ID

Every response includes the X-Trace-Id header (a UUID generated by middleware.TraceID()). Include it in bug reports — server logs are indexed by it and finding the exact request takes seconds.

CORS

cmd/server/main.go configures CORS with AllowOrigins: ["*"] in development. Production restricts this — check config.prod.yaml and the deployed CORS config. Allowed methods: GET, POST, PUT, DELETE, OPTIONS, PATCH. Allowed headers: Origin, Content-Type, Authorization, X-API-Key.

Versioning

The single live version is v1. Breaking changes will ship under /api/v2 rather than being applied in place to v1. Additive changes (new fields, new endpoints) ship into v1 directly.