API

Workjournal publishes an OpenAPI 3.1 specification covering a curated 16-operation subset of the REST API. It powers the public Workjournal Custom GPT in ChatGPT and is available to any HTTP client that speaks standard OAuth 2.0.

OpenAPI 3.1 spec https://api.workjournal.pro/openapi.json public, no auth required

Need the full ~35-tool catalog? See the MCP server.

Authentication

Workjournal does not issue managed long-term API keys or static bearer tokens. Every client authenticates via OAuth with PKCE; the access tokens it receives are short-lived (issued and refreshed by Supabase). Revoking access means signing out — not rotating a leaked secret.

Authorization URLhttps://auth.workjournal.pro/auth/v1/oauth/authorize
Token URLhttps://auth.workjournal.pro/auth/v1/oauth/token
Scopes email
Grant typesauthorization_code, refresh_token
PKCERequired (S256)
Discoveryhttps://auth.workjournal.pro/.well-known/oauth-authorization-server

API reference

Base URL: https://api.workjournal.pro/v1. Every endpoint takes Authorization: Bearer <access_token>.

Workspaces

listWorkspaces

GET /workspaces

Return every workspace the authenticated user owns. Most users have exactly one (their personal workspace). Use the `slug` field on each row when calling other operations that take `workspaceSlug`.

Responses

200 Array of workspace objects.

Field Type Description
data object[]
data[].id string
data[].slug string URL-safe workspace identifier.
data[].name string
data[].owner_id string
data[].tier_code string Pricing tier code: free, plus, pro, admin.
data[].created_at string
  • 401 Missing or invalid bearer token. The user must reauthenticate.

Journals

listJournals

GET /workspaces/{workspaceSlug}/journals

Return every journal in the workspace the caller can see (as owner or contributor). For journals shared from other workspaces use listSharedJournals.

Parameters

Name In Type Required Description
workspaceSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe workspace identifier. Find it via listWorkspaces (the `slug` field on each row). For most users this is their personal workspace.

Responses

200 Paginated list of journal objects.

Field Type Description
data object[]
data[].id string
data[].slug string URL-safe journal identifier, unique within the workspace.
data[].name string
data[].workspace_id string
data[].description string | null
data[].is_public boolean
data[].public_slug string | null
data[].created_at string
data[].contributor object The caller's contributor row on this journal.
data[].prompt object Active writing prompt for this journal. Returned on getJournal only.
total integer
offset integer
limit integer
  • 401 Missing or invalid bearer token. The user must reauthenticate.
  • 403 The user is authenticated but not authorised for this resource (e.g. not a contributor on the journal).
  • 404 The requested resource does not exist or is not visible to the caller.

createJournal

POST /workspaces/{workspaceSlug}/journals

Create a new journal in the workspace. The slug is derived from the name (kebab-case) unless explicitly provided. The caller becomes the owner.

Parameters

Name In Type Required Description
workspaceSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe workspace identifier. Find it via listWorkspaces (the `slug` field on each row). For most users this is their personal workspace.

Request body

Journal creation payload.

Field Type Required Description
name string
length 1–20
yes
description string no
slug string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
no Optional. Derived from name if omitted.

Responses

201 The created journal.

Field Type Description
id string
slug string URL-safe journal identifier, unique within the workspace.
name string
workspace_id string
description string | null
is_public boolean
public_slug string | null
created_at string
contributor object The caller's contributor row on this journal.
contributor.role "owner" | "contributor"
contributor.created_at string
prompt object Active writing prompt for this journal. Returned on getJournal only.
prompt.source "default" | "custom" Whether this is the system default prompt or a custom one assigned to the journal.
prompt.name string | null
prompt.slug string | null
prompt.body string Markdown prompt body. Treat as the writing instruction for entries in this journal.
  • 401 Missing or invalid bearer token. The user must reauthenticate.
  • 402 The requested operation requires a higher tier (Plus or Pro). Surface the message verbatim to the user.
  • 403 The user is authenticated but not authorised for this resource (e.g. not a contributor on the journal).
  • 409 The operation conflicts with the current state (e.g. duplicate slug, tag already assigned).
  • 422 The request body or query parameters failed validation. Inspect `error.message` for the field.

getJournal

GET /workspaces/{workspaceSlug}/journals/{journalSlug}

Return a single journal including its active writing prompt. Read `prompt.body` and apply it as the writing instruction when calling createEntry — it dictates structure, voice, and what sections to include.

Parameters

Name In Type Required Description
workspaceSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe workspace identifier. Find it via listWorkspaces (the `slug` field on each row). For most users this is their personal workspace.
journalSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe journal identifier within the workspace. Find it via listJournals (the `slug` field on each row).

Responses

200 Journal object including the resolved active prompt.

Field Type Description
id string
slug string URL-safe journal identifier, unique within the workspace.
name string
workspace_id string
description string | null
is_public boolean
public_slug string | null
created_at string
contributor object The caller's contributor row on this journal.
contributor.role "owner" | "contributor"
contributor.created_at string
prompt object Active writing prompt for this journal. Returned on getJournal only.
prompt.source "default" | "custom" Whether this is the system default prompt or a custom one assigned to the journal.
prompt.name string | null
prompt.slug string | null
prompt.body string Markdown prompt body. Treat as the writing instruction for entries in this journal.
  • 401 Missing or invalid bearer token. The user must reauthenticate.
  • 403 The user is authenticated but not authorised for this resource (e.g. not a contributor on the journal).
  • 404 The requested resource does not exist or is not visible to the caller.

listSharedJournals

GET /shared-with-me/journals

Return journals the user has been invited into across all workspaces. Each row carries its real `workspace.slug` — use that as `workspaceSlug` for follow-up calls; the slug `shared-with-me` is not a real workspace.

Responses

200 Array of journals shared with the user, with workspace and owner context.

Field Type Description
data object[]
data[].journal object
data[].workspace object
data[].owner object
data[].role "owner" | "contributor"
data[].created_at string
  • 401 Missing or invalid bearer token. The user must reauthenticate.

exportJournal

GET /workspaces/{workspaceSlug}/journals/{journalSlug}/export

Return every entry in the journal as a single payload. JSON includes metadata; md/csv are flat dumps suitable for sharing or backup. Capped at 10000 entries with a `truncated` flag.

Parameters

Name In Type Required Description
workspaceSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe workspace identifier. Find it via listWorkspaces (the `slug` field on each row). For most users this is their personal workspace.
journalSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe journal identifier within the workspace. Find it via listJournals (the `slug` field on each row).
format query "json" | "md" | "csv" no Output format. `json` (default) returns a structured object with the journal + entries array. `md` and `csv` return file payloads with Content-Disposition: attachment.

Responses

200 Export payload. JSON returns the structured object below; md and csv return file payloads with Content-Disposition: attachment.

Field Type Description
journal object
journal.id string
journal.slug string URL-safe journal identifier, unique within the workspace.
journal.name string
journal.workspace_id string
journal.description string | null
journal.is_public boolean
journal.public_slug string | null
journal.created_at string
journal.contributor object The caller's contributor row on this journal.
journal.prompt object Active writing prompt for this journal. Returned on getJournal only.
entries object[]
entries[].id string
entries[].journal_id string
entries[].index integer Per-journal monotonic entry number. The canonical identifier for getEntry / updateEntry / deleteEntry.
entries[].title string
entries[].summary string
entries[].body string Full markdown body. Only present on getEntry, listEntries with include=body, and createEntry/updateEntry responses.
entries[].created_at string
entries[].created_by string | null
entries[].client string | null Free-form label of the client that wrote the entry (e.g. "chatgpt", "claude-code", "cli").
entries[].tags string[] Tag names attached to this entry. Each must be assigned to the journal.
truncated boolean
total_entries integer
  • 401 Missing or invalid bearer token. The user must reauthenticate.
  • 403 The user is authenticated but not authorised for this resource (e.g. not a contributor on the journal).
  • 404 The requested resource does not exist or is not visible to the caller.

Entries

listEntries

GET /workspaces/{workspaceSlug}/journals/{journalSlug}/entries

Return entries in this journal, newest first. Default is slim rows (title + summary); pass include=body for full markdown. Filter by tag with one or more ?tag= params (AND semantics). Use limit to bound the response.

Parameters

Name In Type Required Description
workspaceSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe workspace identifier. Find it via listWorkspaces (the `slug` field on each row). For most users this is their personal workspace.
journalSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe journal identifier within the workspace. Find it via listJournals (the `slug` field on each row).
limit query integer
min 1, max 100
no Max entries to return. Default 50, max 100. For "last N" queries pass the N here.
offset query integer
min 0
no Pagination offset. Combined with limit for paging.
include query string no Comma-separated extras. Currently only `body` is honoured; pass it to include the full markdown body on each entry.
tag query string[] no Tag name to filter on. Repeat for AND semantics (all listed tags must be present). Comma-separated values also work: ?tag=a,b is equivalent to ?tag=a&tag=b.

Responses

200 Paginated list of entries.

Field Type Description
data object[]
data[].id string
data[].journal_id string
data[].index integer Per-journal monotonic entry number. The canonical identifier for getEntry / updateEntry / deleteEntry.
data[].title string
data[].summary string
data[].body string Full markdown body. Only present on getEntry, listEntries with include=body, and createEntry/updateEntry responses.
data[].created_at string
data[].created_by string | null
data[].client string | null Free-form label of the client that wrote the entry (e.g. "chatgpt", "claude-code", "cli").
data[].tags string[] Tag names attached to this entry. Each must be assigned to the journal.
total integer
offset integer
limit integer
  • 401 Missing or invalid bearer token. The user must reauthenticate.
  • 403 The user is authenticated but not authorised for this resource (e.g. not a contributor on the journal).
  • 404 The requested resource does not exist or is not visible to the caller.

createEntry no-confirm

POST /workspaces/{workspaceSlug}/journals/{journalSlug}/entries

The primary write op. Title is a short outcome line (≤80 chars). Summary is 1-3 sentences a future reader can scan (≤500 chars). Body is full markdown. Tags must already be assigned to this journal (Plus+).

Parameters

Name In Type Required Description
workspaceSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe workspace identifier. Find it via listWorkspaces (the `slug` field on each row). For most users this is their personal workspace.
journalSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe journal identifier within the workspace. Find it via listJournals (the `slug` field on each row).

Request body

Entry payload. Follow the journal's prompt.body for structure and voice — fetch it via getJournal.

Field Type Required Description
title string
length 1–80
yes
summary string
length 1–500
yes
body string
length 1–∞
yes Markdown body. No length cap.
client string no Optional client label. Defaults to the OAuth client name (chatgpt for Custom GPT calls).
tags string[] no Tag names. Each must be currently assigned to the journal. Plus+ tier only.

Responses

201 The created entry, including its assigned `index`.

Field Type Description
id string
journal_id string
index integer Per-journal monotonic entry number. The canonical identifier for getEntry / updateEntry / deleteEntry.
title string
summary string
body string Full markdown body. Only present on getEntry, listEntries with include=body, and createEntry/updateEntry responses.
created_at string
created_by string | null
client string | null Free-form label of the client that wrote the entry (e.g. "chatgpt", "claude-code", "cli").
tags string[] Tag names attached to this entry. Each must be assigned to the journal.
  • 401 Missing or invalid bearer token. The user must reauthenticate.
  • 402 The requested operation requires a higher tier (Plus or Pro). Surface the message verbatim to the user.
  • 403 The user is authenticated but not authorised for this resource (e.g. not a contributor on the journal).
  • 404 The requested resource does not exist or is not visible to the caller.
  • 422 The request body or query parameters failed validation. Inspect `error.message` for the field.

getEntry

GET /workspaces/{workspaceSlug}/journals/{journalSlug}/entries/{index}

Return the full entry (title + summary + markdown body + tags + metadata) for the given per-journal index.

Parameters

Name In Type Required Description
workspaceSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe workspace identifier. Find it via listWorkspaces (the `slug` field on each row). For most users this is their personal workspace.
journalSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe journal identifier within the workspace. Find it via listJournals (the `slug` field on each row).
index path integer
min 1
yes Per-journal monotonic entry number, as returned by listEntries or createEntry. Not a UUID.

Responses

200 The entry object.

Field Type Description
id string
journal_id string
index integer Per-journal monotonic entry number. The canonical identifier for getEntry / updateEntry / deleteEntry.
title string
summary string
body string Full markdown body. Only present on getEntry, listEntries with include=body, and createEntry/updateEntry responses.
created_at string
created_by string | null
client string | null Free-form label of the client that wrote the entry (e.g. "chatgpt", "claude-code", "cli").
tags string[] Tag names attached to this entry. Each must be assigned to the journal.
  • 401 Missing or invalid bearer token. The user must reauthenticate.
  • 403 The user is authenticated but not authorised for this resource (e.g. not a contributor on the journal).
  • 404 The requested resource does not exist or is not visible to the caller.

updateEntry

PATCH /workspaces/{workspaceSlug}/journals/{journalSlug}/entries/{index}

Partial update — only the supplied fields change. Sending tags replaces the full tag set on the entry; pass an empty array to clear. Confirms with the user before firing (consequential).

Parameters

Name In Type Required Description
workspaceSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe workspace identifier. Find it via listWorkspaces (the `slug` field on each row). For most users this is their personal workspace.
journalSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe journal identifier within the workspace. Find it via listJournals (the `slug` field on each row).
index path integer
min 1
yes Per-journal monotonic entry number, as returned by listEntries or createEntry. Not a UUID.

Request body

Partial entry payload. Any omitted field is left unchanged.

Field Type Required Description
title string
length 1–80
no
summary string
length 1–500
no
body string
length 1–∞
no
tags string[] no Replaces the entry's tag set. Pass [] to clear all tags.

Responses

200 The updated entry.

Field Type Description
id string
journal_id string
index integer Per-journal monotonic entry number. The canonical identifier for getEntry / updateEntry / deleteEntry.
title string
summary string
body string Full markdown body. Only present on getEntry, listEntries with include=body, and createEntry/updateEntry responses.
created_at string
created_by string | null
client string | null Free-form label of the client that wrote the entry (e.g. "chatgpt", "claude-code", "cli").
tags string[] Tag names attached to this entry. Each must be assigned to the journal.
  • 401 Missing or invalid bearer token. The user must reauthenticate.
  • 402 The requested operation requires a higher tier (Plus or Pro). Surface the message verbatim to the user.
  • 403 The user is authenticated but not authorised for this resource (e.g. not a contributor on the journal).
  • 404 The requested resource does not exist or is not visible to the caller.
  • 422 The request body or query parameters failed validation. Inspect `error.message` for the field.

deleteEntry

DELETE /workspaces/{workspaceSlug}/journals/{journalSlug}/entries/{index}

Permanently remove the entry. The per-journal index is not reused. Confirms with the user before firing (consequential).

Parameters

Name In Type Required Description
workspaceSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe workspace identifier. Find it via listWorkspaces (the `slug` field on each row). For most users this is their personal workspace.
journalSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe journal identifier within the workspace. Find it via listJournals (the `slug` field on each row).
index path integer
min 1
yes Per-journal monotonic entry number, as returned by listEntries or createEntry. Not a UUID.

Responses

204 Entry deleted.

  • 401 Missing or invalid bearer token. The user must reauthenticate.
  • 403 The user is authenticated but not authorised for this resource (e.g. not a contributor on the journal).
  • 404 The requested resource does not exist or is not visible to the caller.

searchEntries

GET /workspaces/{workspaceSlug}/journals/{journalSlug}/entries/search

Search across title, summary, and body of every entry in this journal. Match is full-text and ranked. For "find anything about X" questions, prefer this over listEntries with manual filtering.

Parameters

Name In Type Required Description
workspaceSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe workspace identifier. Find it via listWorkspaces (the `slug` field on each row). For most users this is their personal workspace.
journalSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe journal identifier within the workspace. Find it via listJournals (the `slug` field on each row).
q query string
length 1–∞
yes Search query. Non-empty. Multi-word queries match any token; use longer phrases for more specific results.
limit query integer
min 1, max 100
no Max results. Default 50, max 100.
offset query integer
min 0
no Pagination offset.

Responses

200 Matching entries, newest match first.

Field Type Description
data object[]
data[].id string
data[].journal_id string
data[].index integer Per-journal monotonic entry number. The canonical identifier for getEntry / updateEntry / deleteEntry.
data[].title string
data[].summary string
data[].body string Full markdown body. Only present on getEntry, listEntries with include=body, and createEntry/updateEntry responses.
data[].created_at string
data[].created_by string | null
data[].client string | null Free-form label of the client that wrote the entry (e.g. "chatgpt", "claude-code", "cli").
data[].tags string[] Tag names attached to this entry. Each must be assigned to the journal.
total integer
offset integer
limit integer
  • 401 Missing or invalid bearer token. The user must reauthenticate.
  • 403 The user is authenticated but not authorised for this resource (e.g. not a contributor on the journal).
  • 404 The requested resource does not exist or is not visible to the caller.
  • 422 The request body or query parameters failed validation. Inspect `error.message` for the field.

ragSearch no-confirm

POST /workspaces/{workspaceSlug}/journals/{journalSlug}/rag-search

Rank journal chunks by cosine similarity to a query embedding. Use when the caller asks about a concept rather than a keyword. Pro+ tier; Free/Plus get 402.

Parameters

Name In Type Required Description
workspaceSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe workspace identifier. Find it via listWorkspaces (the `slug` field on each row). For most users this is their personal workspace.
journalSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe journal identifier within the workspace. Find it via listJournals (the `slug` field on each row).

Request body

Query embedding payload. limit defaults to 10, capped at 50.

Field Type Required Description
query string
length 1–2000
yes
limit integer
min 1, max 50
no

Responses

200 Ranked chunk results.

Field Type Description
data object[]
data[].entry_id string
data[].entry_index integer
data[].title string
data[].chunk_index integer
data[].chunk_text string Raw text of the matching chunk.
data[].similarity number Cosine similarity in [-1, 1]. Higher = closer match.
data[].created_at string
query string
limit integer
  • 401 Missing or invalid bearer token. The user must reauthenticate.
  • 402 The requested operation requires a higher tier (Plus or Pro). Surface the message verbatim to the user.
  • 403 The user is authenticated but not authorised for this resource (e.g. not a contributor on the journal).
  • 404 The requested resource does not exist or is not visible to the caller.
  • 422 The request body or query parameters failed validation. Inspect `error.message` for the field.

Tags

listTags

GET /workspaces/{workspaceSlug}/tags

Return the workspace tag registry. Pass `journal_slug` to also include an `assigned` boolean on each row indicating whether the tag is usable on entries in that journal. Free workspaces have zero tags.

Parameters

Name In Type Required Description
workspaceSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe workspace identifier. Find it via listWorkspaces (the `slug` field on each row). For most users this is their personal workspace.
journal_slug query string no Optional journal slug. When set, each tag row carries an `assigned` boolean.

Responses

200 Workspace tag registry, optionally annotated with per-journal `assigned` status.

Field Type Description
data object[]
data[].id string
data[].workspace_id string
data[].name string Kebab-case tag name (canonical, lowercased).
data[].description string | null
data[].created_by string
data[].created_at string
data[].assigned boolean Only present on listTags when ?journal_slug= is set. True if the tag is currently usable on entries in that journal.
  • 401 Missing or invalid bearer token. The user must reauthenticate.
  • 403 The user is authenticated but not authorised for this resource (e.g. not a contributor on the journal).
  • 404 The requested resource does not exist or is not visible to the caller.

createTag

POST /workspaces/{workspaceSlug}/tags

Create a workspace-level tag. Name is kebab-case, max 50 chars. After creation, assign the tag to one or more journals via assignTag before it can be attached to entries. Plus+ tier only.

Parameters

Name In Type Required Description
workspaceSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe workspace identifier. Find it via listWorkspaces (the `slug` field on each row). For most users this is their personal workspace.

Request body

Tag definition.

Field Type Required Description
name string
pattern: `^[a-z0-9][a-z0-9-]{0,49}$`, length 0–50
yes
description string
length 0–200
no

Responses

201 The created tag.

Field Type Description
id string
workspace_id string
name string Kebab-case tag name (canonical, lowercased).
description string | null
created_by string
created_at string
assigned boolean Only present on listTags when ?journal_slug= is set. True if the tag is currently usable on entries in that journal.
  • 401 Missing or invalid bearer token. The user must reauthenticate.
  • 402 The requested operation requires a higher tier (Plus or Pro). Surface the message verbatim to the user.
  • 403 The user is authenticated but not authorised for this resource (e.g. not a contributor on the journal).
  • 409 The operation conflicts with the current state (e.g. duplicate slug, tag already assigned).
  • 422 The request body or query parameters failed validation. Inspect `error.message` for the field.

assignTag

POST /workspaces/{workspaceSlug}/journals/{journalSlug}/tags/{tagName}

Make a registry tag usable on entries in this journal. The tag must already exist via createTag. Returns 409 if the assignment exists. Plus+ tier only.

Parameters

Name In Type Required Description
workspaceSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe workspace identifier. Find it via listWorkspaces (the `slug` field on each row). For most users this is their personal workspace.
journalSlug path string
pattern: `^[a-z0-9][a-z0-9-]{0,29}$`
yes URL-safe journal identifier within the workspace. Find it via listJournals (the `slug` field on each row).
tagName path string
pattern: `^[a-z0-9][a-z0-9-]{0,49}$`, length 0–50
yes Tag name (kebab-case). The tag must already exist in the workspace registry via createTag.

Responses

201 Tag assigned to the journal.

Field Type Description
tag object
tag.id string
tag.name string
tag.description string | null
journal_id string
assigned_at string
  • 401 Missing or invalid bearer token. The user must reauthenticate.
  • 402 The requested operation requires a higher tier (Plus or Pro). Surface the message verbatim to the user.
  • 403 The user is authenticated but not authorised for this resource (e.g. not a contributor on the journal).
  • 404 The requested resource does not exist or is not visible to the caller.
  • 409 The operation conflicts with the current state (e.g. duplicate slug, tag already assigned).