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.