# HydraPub manuscript submission — API guide for AI agents

## Where to start

You need three things:

1. **The base URL** of the journal's API — this instance serves its API at
   `https://pabc-api.databio.org`.
2. **An API key** (`hydrapub_...`) — your human signs in to the journal,
   opens **Settings -> API keys**, creates one, and copies it once (often
   handed to you as `HYDRAPUB_API_KEY` in a `.env` file, with the base URL as
   `HYDRAPUB_API_URL`).
3. **A folder of manuscript materials** — markdown text (or a Google
   Doc/folder), figures, a BibTeX file.

Order of operations: create a draft -> provide the manuscript text -> upload
figures + bibliography -> set metadata -> build the byline (authormark) ->
verify with render + checklist -> submit. Each step is a `curl` below.

How to submit a manuscript to a HydraPub journal end-to-end over plain HTTP.
Every call below is a `curl`; the canonical, always-current contract is
`GET /openapi.json` on the backend (interactive docs at `/docs`). This guide
covers the author-facing submission surface (`/api/journal`); the peer-review
surface has its own guide (`backend/docs/peer-review.md`).

This document is served publicly at `GET /api/public/skill.md`.

## Base URL + authentication

| Environment | Backend |
|---|---|
| This instance | `https://pabc-api.databio.org` |
| Local dev | `http://localhost:8130` |

The submission surface (`/api/journal/*`) authenticates with either credential
in the standard Bearer header:

```
Authorization: Bearer <hydrapub_... API key or Hexclave JWT>
```

Facts an agent must know (from `backend/app/auth.py`):

- Every `/api/journal` endpoint requires the `author` role. The role is
  granted automatically to every account at sign-in (JIT provisioning), so any
  signed-in user can author.
- **A `hydrapub_...` API key authenticates the whole author submission
  surface.** The key resolves to its owning user, and every role and ownership
  check applies unchanged — an agent holding a key acts exactly as that user.
  Submit is still corresponding-author-only.
- **Where a key comes from:** sign in to the journal in a browser, open
  **Settings -> API keys**, create a named key, and copy it — the secret is
  shown exactly once.
- A **Hexclave JWT** (the browser-session access token) works in the same
  Bearer slot — it is the browser's credential. Key *management*
  (mint/list/revoke) is JWT-only: a leaked key can never mint more keys.
- **Dev bypass:** with `HYDRAPUB_AUTH_DISABLED=true` (the `deployment/dev.env`
  default) verification is skipped and every request resolves to a fixed
  dev user — no header needed. An agent driving a local dev instance can omit
  auth entirely.
- Ownership matters: most steps allow active collaborators (edit tier), but
  **submit is corresponding-author-only**.

```bash
BASE=https://pabc-api.databio.org
AUTH="Authorization: Bearer $HYDRAPUB_API_KEY"   # omit on a dev instance with auth disabled
```

One more credential appears mid-flow: the **authormark edit token**, a
capability credential for the byline editor. It is returned by the authormark
link step and travels in `X-Edit-Token` against the authormark service — never
in the Bearer slot, and never mixed with your journal credential.

## What a submission consists of

| Ingredient | Format | Where it goes |
|---|---|---|
| Manuscript text | Pandoc-flavored Markdown (UTF-8) | version source (`manuscript.md`) |
| Figures | `.svg .pdf .png .jpg .jpeg .webp .tiff` | `fig/` in the version dir |
| References | One BibTeX file | `references.bib` |
| Supplementary files | anything | `supp/` |
| Cover letter (optional) | any file | `cover_letter.<ext>` |
| Metadata | JSON | title, abstract, keywords, license, funding, COI |
| Byline | authormark paper | frozen into the record at submit |

### Markdown format (what the renderer accepts)

The server renders with pandoc:
`--from=markdown+tex_math_dollars+pipe_tables+fenced_divs --to=html5 --mathjax`
(`backend/app/services/render_service.py`). Practically:

- Standard pandoc Markdown: `#` headings, emphasis, lists, tables
  (pipe tables), fenced divs, `$...$` / `$$...$$` TeX math.
- **Figures**: `![Caption](fig/plot.png)` with a path **relative to the
  version root** that matches an uploaded file exactly (inline
  `<img src="...">` also works). Absolute paths, `http(s)://`, and `data:`
  URIs are not resolved against uploads. Optionally use pandoc-crossref
  conventions — `{#fig:label}` on the image and `@fig:label` in text — which
  the figure-order check understands; otherwise figure order is positional.
- **Citations**: pandoc citeproc syntax — `[@key]`, `[@key1; @key2]`, or bare
  `@key`. Every cited key must exist in the uploaded `.bib`, or the
  submission checklist fails.
- The manuscript upload must be valid UTF-8 `.md`/`.markdown` (400 otherwise).

## The submission walk

This is the exact sequence a working reference client (sciquill's `hydrapub`
publisher plugin) performs.

### 1. Create a draft

```bash
curl -sX POST "$BASE/api/journal/articles" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"title": "My paper title"}'
# -> ArticleDetail: {"id": 1, "public_id": "pabc-2026-0001", "stage": "draft", ...}
```

Creates the draft article plus an empty version 1. Optional field `track`
selects a submission track; `GET /api/journal/config/tracks` lists the slugs
you may use. All later calls use the article `id`:

```bash
AID=1
P="$BASE/api/journal/articles/$AID"
```

### 2. Provide the manuscript text (pick ONE of three sources)

**a) Set the markdown directly** (simplest for agents):

```bash
curl -sX PUT "$P/versions/1/source" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"markdown": "# Title\n\nBody text with a citation [@smith2020] and a figure.\n\n![Overview](fig/overview.png)\n"}'
```

(Equivalent: upload a `.md` file with `kind=manuscript` on the files endpoint —
same result, plus it clears any Google-Doc provenance.)

**b) Import a single Google Doc** (needs the Drive integration configured —
check `GET $P/../../config/gdoc`, i.e. `/api/journal/config/gdoc`; share the
Doc with the returned `service_account_email`, Viewer access):

```bash
curl -sX POST "$P/versions/1/import-gdoc" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"doc_id_or_url": "https://docs.google.com/document/d/<id>/edit"}'
```

The Doc is exported to Markdown; embedded images are extracted into `fig/`
and the image references rewritten. `POST $P/versions/1/resync-gdoc` re-pulls
the same Doc later.

**c) Import a whole Google Drive folder** (manuscript Doc + figures + `.bib` +
supplementary + cover letter in one link-shared folder — "Anyone with the
link", Viewer):

```bash
# Inspect: list the folder, get heuristic role suggestions.
curl -sX POST "$P/versions/1/gdoc-folder/inspect" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"folder_url": "https://drive.google.com/drive/folders/<id>"}'
# -> {"folder_id": "...", "items": [{"file_id", "name", "mime_type", "suggested_role"}, ...]}

# Import: confirm an item->role mapping. Roles:
#   manuscript | figure | bibliography | supplementary | cover_letter | ignore
curl -sX POST "$P/versions/1/gdoc-folder/import" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"folder_id": "<id>", "items": [
        {"file_id": "f1", "name": "paper", "role": "manuscript"},
        {"file_id": "f2", "name": "figure1.svg", "role": "figure"},
        {"file_id": "f3", "name": "refs.bib", "role": "bibliography"}]}'
```

Validation (422): exactly one `manuscript` (must be a Google Doc), at most one
`bibliography`, at most one `cover_letter`. Google Drawings export as SVG
figures; the `.bib` lands as `references.bib`.
`POST $P/versions/1/gdoc-folder/resync` replays the stored mapping.

### 3. Upload figures, bibliography, supplementary files

One multipart request per file (`kind` = `figure | supp | bib | manuscript |
cover_letter`; there is no bulk endpoint — loop):

```bash
curl -sX POST "$P/versions/1/files" -H "$AUTH" \
  -F "file=@overview.png" -F "kind=figure"

curl -sX POST "$P/versions/1/files" -H "$AUTH" \
  -F "file=@references.bib" -F "kind=bib"
```

- `figure` → stored at `fig/<name>`; extension must be one of
  `.svg .pdf .png .jpg .jpeg .webp .tiff` (400 otherwise). The stored name
  must match the manuscript's image path (`![...](fig/overview.png)` ⇔
  upload named `overview.png`).
- `bib` → always stored as `references.bib`, whatever the upload was called.
- `supp` → `supp/<name>`; `cover_letter` → `cover_letter.<ext>`.
- `DELETE $P/versions/1/files/{relpath}` removes a file;
  `GET $P/versions/1/files/{relpath}` serves it back.

### 4. Set metadata

```bash
curl -sX PUT "$P/versions/1/metadata" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"title": "My paper title",
       "abstract": "One-paragraph abstract.",
       "keywords": ["genomics", "epigenetics"],
       "license": "CC-BY-4.0",
       "funding": [{"funder_name": "NIH", "award_id": "R01-XYZ", "funder_id": null}],
       "coi_statement": "The authors declare no competing interests."}'
```

`title` and `abstract` are required (the checklist enforces non-empty).

`license` is optional and journal-specific: each journal offers its own set (a
journal may offer exactly one, in which case there is nothing to choose). Omit
the field to get the journal's default, or ask for the offer list first:

```bash
curl -s "$P/config/licenses" -H "$AUTH"
# -> {"licenses": [{"code": "CC-BY-4.0", "label": "...", "url": "..."}], "default": "CC-BY-4.0"}
```

A code the journal does not offer is rejected with 422.

### 5. Build the byline (authormark)

The byline (authors, order, affiliations, CRediT) lives in a linked
**authormark** paper, not in journal metadata.

```bash
curl -sX POST "$P/versions/1/authormark" -H "$AUTH"
# -> {"slug": "...", "edit_url": "https://<authormark>/p/<slug>?edit_token=<tok>", "view_url": ...}
```

Idempotent — call again to re-fetch the link plus a live author summary. Then
populate authors through authormark's capability PATCH grammar. Strip the
query from `edit_url`, append `.json`, and send the token in `X-Edit-Token`
on an **unauthenticated** client (never leak your journal Bearer credential
to authormark):

```bash
AM_URL="https://<authormark>/p/<slug>.json"
TOK="X-Edit-Token: <edit_token>"

# Create authors (response returns their ids, in order):
curl -sX PATCH "$AM_URL" -H "$TOK" -H "Content-Type: application/json" \
  -d '{"operations": [
        {"op": "add_author", "given": "Ada", "family": "Lovelace"},
        {"op": "add_author", "given": "Grace", "family": "Hopper"}]}'

# Mark the corresponding author + emails:
curl -sX PATCH "$AM_URL" -H "$TOK" -H "Content-Type: application/json" \
  -d '{"operations": [
        {"op": "update_author", "author_id": 1, "email": "ada@example.org", "corresponding": true}]}'
```

The checklist requires: at least one author, every author has given + family
names, **exactly one** corresponding author with a valid email. ORCID iDs are
reported but never required.

### 6. Verify: render + checklist

```bash
curl -s "$P/versions/1/render" -H "$AUTH"      # 422 with pandoc stderr on bad markdown
curl -s "$P/versions/1/checklist" -H "$AUTH"   # {"items": [...], "all_passed": true|false}
```

Checklist items (each `{key, label, passed, detail}`): `has_source`,
`renders_clean`, `has_title`, `has_abstract`, `has_authors`,
`figures_resolve` (every relative image path in the markdown matches an
uploaded file), `citations_resolvable` (every `@key` exists in the `.bib`),
`license_selected`, and — on revision versions only — `has_response`. Fix
anything failing; the `detail` string says what is wrong.

### 7. Submit

```bash
curl -sX POST "$P/submit" -H "$AUTH"
```

Corresponding-author-only. 409 with a reason if the checklist does not fully
pass or the stage is not submittable. On success the version and byline are
**frozen** (immutable — content endpoints return 409 from now on) and the
article moves `draft -> submitted`.

### 8. Watch the automated on-submit checks (optional)

Enabled automated checks (figure QC, similarity, citation & claim, AI
summaries) fire in the background at submit. Poll:

```bash
curl -s "$P/versions/1/checks" -H "$AUTH"
# {"checks": [{"kind", "status", "digest", "summary", "detail", ...}], "all_settled": bool}
```

Stop polling when `all_settled` is true. If the journal runs the AI-summaries
pipeline, the submitter must edit/approve the generated summaries before
review starts (see `backend/docs/summaries.md`).

## Revisions

When the editor requests changes the article stage becomes
`revision_requested`:

```bash
curl -sX POST "$P/versions" -H "$AUTH"                      # create v2, seeded from v1 (files copied)
curl -sX PUT "$P/versions/2/source" -H "$AUTH" ...          # edit as above, but versions/2
curl -sX PUT "$P/versions/2/response" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"response": "Point-by-point response to the reviewers..."}'   # required for v2+
curl -sX POST "$P/submit" -H "$AUTH"                        # resubmit
```

## Collaborators

The corresponding author can grant co-authors edit access (separate from the
byline — a byline entry grants no login access and vice versa):

```bash
curl -sX POST "$P/collaborators" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"email": "coauthor@example.org"}'
```

## Error-code cheat sheet

| Code | Meaning |
|---|---|
| 401 | Missing/invalid credential (revoked or expired key, bad JWT) |
| 403 | Missing role, not your article, or an unshared Google Doc/folder |
| 409 | Frozen version, failed checklist at submit, or illegal stage transition |
| 422 | Render failure (pandoc stderr in `detail`), bad folder mapping, bad track |
| 400 | Wrong file type/encoding, bad Doc/folder id |
| 502/503 | Drive or authormark upstream problems / integration unconfigured |

## Ground truth

If this document disagrees with the running server, trust `GET /openapi.json`.
Source of record: `backend/app/routers/submission.py`,
`backend/app/services/checklist_service.py`,
`backend/app/services/render_service.py`, `backend/docs/submission-ingestion.md`.
