Iron Log: the wire
This page shows what Iron Log sends and receives over HTTP. The generated Axum server speaks JSON:API 1.1: every request and response body is application/vnd.api+json, except the event streams, which are text/event-stream. The wire contract is the full specification, the server transports guide explains how the routes are generated, and Upgrading lists what changed for existing clients.
The Tauri app reaches the same API over IPC. To serve it over HTTP without Tauri, run the iron-log-http binary, which creates its SQLite tables from the generated SeaORM entities:
cd examples/iron-log/src-tauriPORT=39104 cargo run --bin iron-log-http # without PORT it serves on 3004It starts with an empty in-memory database (IRON_LOG_DB=iron-log.sqlite keeps the data in a file instead). Every pair below was captured from a real run of that binary with curl -sg -i. The requests ran in the order shown, so each one sees the writes before it. Response bodies are formatted with jq. Of the response headers, only the status line, Content-Type, Location, Allow and Vary are shown, plus Cache-Control on the event stream.
The four resource types are exercises, tags, workouts and workout-sets. Iron Log configures no pagination, so its lists are not paged; they sort and include. Its id strategy is IdStrategy::Provided: the client supplies every id. Beside the resources it serves one custom op, GET /api/stats/workout, and two event streams under /api/events/, which publish every store change. Iron Log MD serves the same four resources from a markdown vault, with documents of the same shape.
Contents
- Create an exercise
- Create a workout with a tag
- Create a set
- List sets, heaviest first, with their exercise
- One workout with its tags
- Update an attribute and a relationship
- Read a relationship
- Delete a set
- A custom op
- Watch set changes as they happen
- Errors
Create an exercise
Section titled “Create an exercise”The client sends the id. An exercise has no relationships, so the resource object has no relationships member.
POST /api/exercises HTTP/1.1Accept: application/vnd.api+jsonContent-Type: application/vnd.api+json
{ "data": { "type": "exercises", "id": "back-squat", "attributes": { "name": "Back squat", "muscle_group": "legs", "equipment": "barbell" } }}HTTP/1.1 201 Createdcontent-type: application/vnd.api+jsonvary: Acceptlocation: /api/exercises/back-squat
{ "jsonapi": { "version": "1.1" }, "links": { "self": "/api/exercises/back-squat" }, "data": { "type": "exercises", "id": "back-squat", "attributes": { "name": "Back squat", "muscle_group": "legs", "equipment": "barbell", "notes": null }, "links": { "self": "/api/exercises/back-squat" } }}Two tags, strength and volume, were created the same way next; those two pairs are omitted.
Create a workout with a tag
Section titled “Create a workout with a tag”tags is a many-to-many relationship, stored in the workout_tags junction table.
POST /api/workouts HTTP/1.1Accept: application/vnd.api+jsonContent-Type: application/vnd.api+json
{ "data": { "type": "workouts", "id": "w-2026-06-03", "attributes": { "name": "Squat volume", "date": "2026-06-03", "duration_minutes": 50, "created_at": "2026-06-03T07:15:00Z" }, "relationships": { "tags": { "data": [ { "type": "tags", "id": "strength" } ] } } }}HTTP/1.1 201 Createdcontent-type: application/vnd.api+jsonvary: Acceptlocation: /api/workouts/w-2026-06-03
{ "jsonapi": { "version": "1.1" }, "links": { "self": "/api/workouts/w-2026-06-03" }, "data": { "type": "workouts", "id": "w-2026-06-03", "attributes": { "name": "Squat volume", "date": "2026-06-03", "duration_minutes": 50, "notes": null, "created_at": "2026-06-03T07:15:00Z" }, "relationships": { "tags": { "links": { "self": "/api/workouts/w-2026-06-03/relationships/tags", "related": "/api/workouts/w-2026-06-03/tags" }, "data": [ { "type": "tags", "id": "strength" } ] } }, "links": { "self": "/api/workouts/w-2026-06-03" } }}Create a set
Section titled “Create a set”A set has two to-one relationships, workout and exercise, and both targets must exist.
POST /api/workout-sets HTTP/1.1Accept: application/vnd.api+jsonContent-Type: application/vnd.api+json
{ "data": { "type": "workout-sets", "id": "w-2026-06-03-squat-1", "attributes": { "set_number": 1, "weight_grams": 100000, "reps": 5 }, "relationships": { "workout": { "data": { "type": "workouts", "id": "w-2026-06-03" } }, "exercise": { "data": { "type": "exercises", "id": "back-squat" } } } }}HTTP/1.1 201 Createdcontent-type: application/vnd.api+jsonvary: Acceptlocation: /api/workout-sets/w-2026-06-03-squat-1
{ "jsonapi": { "version": "1.1" }, "links": { "self": "/api/workout-sets/w-2026-06-03-squat-1" }, "data": { "type": "workout-sets", "id": "w-2026-06-03-squat-1", "attributes": { "set_number": 1, "weight_grams": 100000, "reps": 5, "rpe": null, "notes": null }, "relationships": { "workout": { "links": { "self": "/api/workout-sets/w-2026-06-03-squat-1/relationships/workout", "related": "/api/workout-sets/w-2026-06-03-squat-1/workout" }, "data": { "type": "workouts", "id": "w-2026-06-03" } }, "exercise": { "links": { "self": "/api/workout-sets/w-2026-06-03-squat-1/relationships/exercise", "related": "/api/workout-sets/w-2026-06-03-squat-1/exercise" }, "data": { "type": "exercises", "id": "back-squat" } } }, "links": { "self": "/api/workout-sets/w-2026-06-03-squat-1" } }}A second set, w-2026-06-03-squat-2 at 105000 grams, was created the same way; that pair is omitted.
List sets, heaviest first, with their exercise
Section titled “List sets, heaviest first, with their exercise”sort=-weight_grams sorts descending and include=exercise adds the shared exercise to included once, though both sets link it.
GET /api/workout-sets?sort=-weight_grams&include=exercise HTTP/1.1Accept: application/vnd.api+jsonHTTP/1.1 200 OKcontent-type: application/vnd.api+jsonvary: Accept
{ "jsonapi": { "version": "1.1" }, "links": { "self": "/api/workout-sets?sort=-weight_grams&include=exercise" }, "data": [ { "type": "workout-sets", "id": "w-2026-06-03-squat-2", "attributes": { "set_number": 2, "weight_grams": 105000, "reps": 5, "rpe": null, "notes": null }, "relationships": { "workout": { "links": { "self": "/api/workout-sets/w-2026-06-03-squat-2/relationships/workout", "related": "/api/workout-sets/w-2026-06-03-squat-2/workout" }, "data": { "type": "workouts", "id": "w-2026-06-03" } }, "exercise": { "links": { "self": "/api/workout-sets/w-2026-06-03-squat-2/relationships/exercise", "related": "/api/workout-sets/w-2026-06-03-squat-2/exercise" }, "data": { "type": "exercises", "id": "back-squat" } } }, "links": { "self": "/api/workout-sets/w-2026-06-03-squat-2" } }, { "type": "workout-sets", "id": "w-2026-06-03-squat-1", "attributes": { "set_number": 1, "weight_grams": 100000, "reps": 5, "rpe": null, "notes": null }, "relationships": { "workout": { "links": { "self": "/api/workout-sets/w-2026-06-03-squat-1/relationships/workout", "related": "/api/workout-sets/w-2026-06-03-squat-1/workout" }, "data": { "type": "workouts", "id": "w-2026-06-03" } }, "exercise": { "links": { "self": "/api/workout-sets/w-2026-06-03-squat-1/relationships/exercise", "related": "/api/workout-sets/w-2026-06-03-squat-1/exercise" }, "data": { "type": "exercises", "id": "back-squat" } } }, "links": { "self": "/api/workout-sets/w-2026-06-03-squat-1" } } ], "included": [ { "type": "exercises", "id": "back-squat", "attributes": { "name": "Back squat", "muscle_group": "legs", "equipment": "barbell", "notes": null }, "links": { "self": "/api/exercises/back-squat" } } ]}One workout with its tags
Section titled “One workout with its tags”include=tags works on a to-many relationship too.
GET /api/workouts/w-2026-06-03?include=tags HTTP/1.1Accept: application/vnd.api+jsonHTTP/1.1 200 OKcontent-type: application/vnd.api+jsonvary: Accept
{ "jsonapi": { "version": "1.1" }, "links": { "self": "/api/workouts/w-2026-06-03?include=tags" }, "data": { "type": "workouts", "id": "w-2026-06-03", "attributes": { "name": "Squat volume", "date": "2026-06-03", "duration_minutes": 50, "notes": null, "created_at": "2026-06-03T07:15:00Z" }, "relationships": { "tags": { "links": { "self": "/api/workouts/w-2026-06-03/relationships/tags", "related": "/api/workouts/w-2026-06-03/tags" }, "data": [ { "type": "tags", "id": "strength" } ] } }, "links": { "self": "/api/workouts/w-2026-06-03" } }, "included": [ { "type": "tags", "id": "strength", "attributes": { "name": "Strength" }, "links": { "self": "/api/tags/strength" } } ]}Update an attribute and a relationship
Section titled “Update an attribute and a relationship”The PATCH sets notes and replaces tags; the store rewrites the junction rows in the order sent.
PATCH /api/workouts/w-2026-06-03 HTTP/1.1Accept: application/vnd.api+jsonContent-Type: application/vnd.api+json
{ "data": { "type": "workouts", "id": "w-2026-06-03", "attributes": { "notes": "Moved fast between sets." }, "relationships": { "tags": { "data": [ { "type": "tags", "id": "strength" }, { "type": "tags", "id": "volume" } ] } } }}HTTP/1.1 200 OKcontent-type: application/vnd.api+jsonvary: Accept
{ "jsonapi": { "version": "1.1" }, "links": { "self": "/api/workouts/w-2026-06-03" }, "data": { "type": "workouts", "id": "w-2026-06-03", "attributes": { "name": "Squat volume", "date": "2026-06-03", "duration_minutes": 50, "notes": "Moved fast between sets.", "created_at": "2026-06-03T07:15:00Z" }, "relationships": { "tags": { "links": { "self": "/api/workouts/w-2026-06-03/relationships/tags", "related": "/api/workouts/w-2026-06-03/tags" }, "data": [ { "type": "tags", "id": "strength" }, { "type": "tags", "id": "volume" } ] } }, "links": { "self": "/api/workouts/w-2026-06-03" } }}Read a relationship
Section titled “Read a relationship”The relationship endpoint answers with linkage only: the tag identifiers the PATCH above wrote, in order, plus self and related links.
GET /api/workouts/w-2026-06-03/relationships/tags HTTP/1.1Accept: application/vnd.api+jsonHTTP/1.1 200 OKcontent-type: application/vnd.api+jsonvary: Accept
{ "jsonapi": { "version": "1.1" }, "links": { "self": "/api/workouts/w-2026-06-03/relationships/tags", "related": "/api/workouts/w-2026-06-03/tags" }, "data": [ { "type": "tags", "id": "strength" }, { "type": "tags", "id": "volume" } ]}Delete a set
Section titled “Delete a set”A DELETE removes the second set and answers 204 with no body.
DELETE /api/workout-sets/w-2026-06-03-squat-2 HTTP/1.1Accept: application/vnd.api+jsonHTTP/1.1 204 No Contentvary: AcceptA custom op
Section titled “A custom op”A custom op answers with a document that has meta.result and no data. The example’s get_workout is a stub that always returns zeros, so the numbers here do not count the workout created above; the shape is the point.
GET /api/stats/workout HTTP/1.1Accept: application/vnd.api+jsonHTTP/1.1 200 OKcontent-type: application/vnd.api+jsonvary: Accept
{ "jsonapi": { "version": "1.1" }, "meta": { "result": { "total_count": 0, "total_duration_minutes": 0 } }}Watch set changes as they happen
Section titled “Watch set changes as they happen”GET /api/events/activity-for-kind/{kind} is an event stream of the store changes to one kind of entity. Each frame is an event: line naming the op, a data: line holding the Activity as {"meta":{"result":…}} (it is not an entity, so it gets the custom-op envelope), and an id: line, because this op is resumable: a client that reconnects sends the last id back as Last-Event-ID (or ?resume=); this example keeps no history, so nothing is replayed.
This stream was held open for three seconds (curl -N --max-time 3) while the next requests ran: a PATCH of the remaining set, a PATCH of the workout’s duration_minutes (pair omitted), and a DELETE of the set. seq counts every store change since the server started, so the workout update is the missing 10; this stream carries only workout_set changes. GET /api/events/activity-feed carries every kind, with no id: lines.
GET /api/events/activity-for-kind/workout_set HTTP/1.1Accept: text/event-streamHTTP/1.1 200 OKcontent-type: text/event-streamcache-control: no-cache
event: activity-for-kinddata: {"meta":{"result":{"seq":9,"kind":"workout_set","id":"w-2026-06-03-squat-1"}}}id: 9
event: activity-for-kinddata: {"meta":{"result":{"seq":11,"kind":"workout_set","id":"w-2026-06-03-squat-1"}}}id: 11PATCH /api/workout-sets/w-2026-06-03-squat-1 HTTP/1.1Accept: application/vnd.api+jsonContent-Type: application/vnd.api+json
{ "data": { "type": "workout-sets", "id": "w-2026-06-03-squat-1", "attributes": { "rpe": 8 } }}HTTP/1.1 200 OKcontent-type: application/vnd.api+jsonvary: Accept
{ "jsonapi": { "version": "1.1" }, "links": { "self": "/api/workout-sets/w-2026-06-03-squat-1" }, "data": { "type": "workout-sets", "id": "w-2026-06-03-squat-1", "attributes": { "set_number": 1, "weight_grams": 100000, "reps": 5, "rpe": 8, "notes": null }, "relationships": { "workout": { "links": { "self": "/api/workout-sets/w-2026-06-03-squat-1/relationships/workout", "related": "/api/workout-sets/w-2026-06-03-squat-1/workout" }, "data": { "type": "workouts", "id": "w-2026-06-03" } }, "exercise": { "links": { "self": "/api/workout-sets/w-2026-06-03-squat-1/relationships/exercise", "related": "/api/workout-sets/w-2026-06-03-squat-1/exercise" }, "data": { "type": "exercises", "id": "back-squat" } } }, "links": { "self": "/api/workout-sets/w-2026-06-03-squat-1" } }}DELETE /api/workout-sets/w-2026-06-03-squat-1 HTTP/1.1Accept: application/vnd.api+jsonHTTP/1.1 204 No Contentvary: AcceptErrors
Section titled “Errors”Every error is an errors[] document with status, code, title and detail, plus source when one part of the request is to blame.
A subscription that fails before the stream opens is answered with an error document instead of a stream. An unknown kind is the example’s ActivityKindNotFound error, so 404:
GET /api/events/activity-for-kind/plates HTTP/1.1HTTP/1.1 404 Not Foundcontent-type: application/vnd.api+jsonvary: Accept
{ "jsonapi": { "version": "1.1" }, "errors": [ { "status": "404", "code": "activity_kind_not_found", "title": "Not Found", "detail": "Activity kind not found: plates" } ]}An Accept header that lists the JSON:API media type only with an extension is 406, because the server supports none:
GET /api/workouts HTTP/1.1Accept: application/vnd.api+json; ext="https://example.com/ext"HTTP/1.1 406 Not Acceptablecontent-type: application/vnd.api+jsonvary: Accept
{ "jsonapi": { "version": "1.1" }, "errors": [ { "status": "406", "code": "not_acceptable", "title": "Not Acceptable", "detail": "this server responds only with application/vnd.api+json, with no parameters other than profile", "source": { "header": "Accept" } } ]}