Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Medium (medium)

Medium is an article provider with a legacy API. Rustiz sends a self-issued Medium integration token as a Bearer token, reads the user’s profile and publications, and creates a story either on the user’s profile (public) or in a publication (always as a draft).

  • Rust source: crates/rustiz-providers/src/medium.rs
  • Upstream port: medium.provider.ts
  • Editor: Markdown
  • Connection mode: customFields

Important: Medium stopped issuing new integration tokens and does not allow new API integrations. Existing tokens continue to work. If the account does not already have a token, the medium provider cannot be used. See https://help.medium.com/hc/en-us/articles/213480228-API-Importing.

Overview

PropertyValue
Identifiermedium
Display nameMedium
Connection modecustomFields (legacy integration token)
Editormarkdown
Channel kindsocial (article content)
Comments / threadsNo. supports_comments = false; only the first PostDetails is published
Pending / async publishingNo. supports_pending = false; returns PostStatus::Completed
AnalyticsNo. supports_analytics = false
Post analyticsNo. supports_post_analytics = false
MentionsNo. supports_mentions = false
is_between_stepsfalse
convert_to_jpegfalse
max_concurrent_job3
Character limit (max_length)100000 characters for the story body
APIMedium API v1, https://api.medium.com/v1

Server Environment Variables

required_env() returns no variables:

#![allow(unused)]
fn main() {
fn required_env(&self) -> &'static [&'static str] {
    &[]
}
}

required_env_alternatives() is not overridden. rustiz providers --status reports medium as configured with no server environment.

VariableRequiredDefaultPurpose
MEDIUM_BASE_URLNohttps://api.medium.comOverrides the API origin. The value is trimmed of a trailing slash. The paths added by the provider include /v1/..., so the override should be the origin only.

Step-by-Step Developer App Setup

There is no developer app to create in Rustiz. The provider does not implement Medium’s browser OAuth flow at all; it only accepts a self-issued integration token.

  1. Sign in to Medium.

  2. Open Settings → Security and apps → Integration tokens (https://medium.com/me/settings). Medium’s own API documentation calls the section Integration Tokens.

  3. If the page still offers Get integration token, create one with a descriptive name such as Rustiz. Medium states that it is no longer issuing new tokens, so many accounts will not see this option.

  4. Copy the token and verify it:

    curl -s https://api.medium.com/v1/me \
      -H "Authorization: Bearer $MEDIUM_TOKEN"
    

    The response is {"data":{"id","username","name","url","imageUrl"}}, which is exactly what Rustiz stores.

Products and permissions

  • No products or APIs to enable.
  • No OAuth scopes in this flow. A legacy OAuth integration would have used basicProfile, listPublications, publishPost and optionally uploadImage, but Rustiz never performs that flow.
  • The token is account-wide. It can publish on the profile and create drafts in publications where the user is a writer or editor.

Sandbox / testing mode

Medium has no sandbox. The API documentation explicitly says there is no sandbox and recommends a separate testing account; requests act on production data at medium.com.

  • The safest test is a publication post: Rustiz uses publishStatus: "draft" whenever settings.publication is set. A draft in a publication stays pending until an editor publishes it.
  • A post without publication uses publishStatus: "public" and is live immediately.
  • GET /v1/me and GET /v1/users/{id}/publications are safe read-only checks.

Production app review

There is no app review because Medium is not accepting new integrations. Existing tokens are not reviewed or re-approved. The practical “review” is Medium’s decision to keep the legacy API alive; do not build a new onboarding flow that assumes new users can obtain a token.

Connecting a Channel (CLI & MCP)

The credential payload has exactly one key:

KeyTypeValidation regexNotes
apiKeypassword/^.{3,}$/Legacy Medium integration token

CLI

{ "apiKey": "2f1a9c...medium-token" }
rustiz channels connect medium --fields medium-key.json

printf '%s' '{"apiKey":"2f1a9c...medium-token"}' \
  | rustiz channels connect medium --fields -

Immediate result:

{ "connected": true, "pending": false, "name": "Your Medium name" }

MCP

{
  "provider": "medium",
  "customFields": {
    "apiKey": "2f1a9c...medium-token"
  }
}

No follow-up step. Call integrationList for the channel id. Use --fields / customFields; --external-url / externalUrl is not the credential path for this provider.

Provider functions (triggerTool)

FunctionParametersReturnsNotes
publicationsnoneThe data array from GET /v1/users/{id}/publicationsUses integration.internal_id (the Medium user id) as the path id, not the channel database id. Each item has id, name, description, url, imageUrl.

MCP:

{ "integrationId": "int_...", "methodName": "publications" }

The function has an empty input_schema / dataSchema.

Post Settings & Media Validation

settings_schema()

CLI note: the simple rustiz posts create --channel <id> --content "..." form cannot pass provider settings. Use rustiz posts create --json post.json with a CreatePostDto (type, date, posts[].settings, …) when any of these fields are required.

FieldTypeRequiredValidation / behavior
titlestringYesminLength: 2; checked. Medium itself ignores titles longer than 100 characters and synthesizes one from the content.
subtitlestringYesminLength: 2; checked. It is never sent to Medium — the provider comment says Medium’s API does not accept it on publish. It exists for parity with upstream.
canonicalstringNoSent as canonicalUrl. If it contains (post:, URL validation is skipped.
publicationstringNoMedium publication id. When set, the story is created under the publication with publishStatus: "draft". When empty, it is published on the user’s profile with publishStatus: "public".
tagsarrayNomaxItems: 4; each item needs a string value (the tag name sent to Medium) and a string label (editor display only). Medium only uses the first three tags and ignores tags longer than 25 characters.

The schema’s required array is ["title", "subtitle"]. This surprises people who expect Medium’s subtitle to be optional: Rustiz rejects a story without a subtitle even though the subtitle is discarded.

Example object form:

{
  "title": "A week with SQLite",
  "subtitle": "Notes from a small database",
  "canonical": "https://blog.example/sqlite-week",
  "publication": "",
  "tags": [
    { "value": "rust", "label": "Rust" },
    { "value": "sqlite", "label": "SQLite" }
  ]
}

MCP settings array:

{
  "settings": [
    { "key": "title", "value": "A week with SQLite" },
    { "key": "subtitle", "value": "Notes from a small database" },
    { "key": "tags", "value": [
      { "value": "rust", "label": "Rust" },
      { "value": "sqlite", "label": "SQLite" }
    ] }
  ]
}

The engine adds "__type": "medium" automatically if missing.

validate_settings() rules

  1. title must exist, be a string and have at least 2 characters.
  2. subtitle must exist, be a string and have at least 2 characters.
  3. canonical, if present and a string, must be a valid URL unless it contains (post:.
  4. publication, if present and not null, must be a string. An empty string is accepted and means “publish on the profile”.
  5. tags, if present and not null, must be an array of at most 4 items. Each item must be an object with a string value and a string label. The validator does not check the 25-character Medium tag limit or the first-three rule; Medium applies those itself.

Media requirements

  • There is no main_image field in the schema. Medium is not sent a cover image and Rustiz does not call Medium’s image upload endpoint.
  • The post media / attachments array is ignored.
  • Images can only appear if the Markdown body references public image URLs; Medium side-loads images referenced by <img> tags in the content.
  • The Rustiz media validator is the trait default (Ok(None)), so there are no provider-specific media checks.

Plugs

No @Plug or @PostPlug methods. run_plug() and run_post_plug() use the trait defaults and return ProviderError::Unsupported.

Implementation Notes & Gotchas

  • Medium is closed to new integrations. This is the dominant operational fact. The provider will connect with any 3+ character string that Medium accepts as a token, but Medium will reject tokens it did not issue. New users cannot onboard.
  • Publication posts are always drafts. post() sets publishStatus: "draft" whenever publication is non-empty, even for a user who is an editor and could publish directly. This is inherited from upstream Postiz and is a safe default for writers, but the story will not appear publicly until an editor publishes it.
  • A profile post is always public. With no publication, the status is "public"; there is no unlisted setting in Rustiz.
  • Subtitle is mandatory but discarded. The schema and validator require it, and post() never includes it in the request body. If you need a subtitle visually, put it in the Markdown content.
  • Tags are capped at 4 in Rustiz, 3 in Medium. The fourth tag is silently ignored by Medium. label is not sent; value is.
  • Title >100 characters is ignored by Medium. Rustiz has no title max, so a long title passes validation and Medium synthesizes a title from the first content instead.
  • (post: canonical placeholder is sent if present. Validation skips URL checks for it, but post() sends any non-empty canonical string as canonicalUrl. Omit the field to leave the canonical URL out.
  • No refresh. Tokens are stored with a 100-year expires_in and an empty refresh token; refresh_token() returns an empty access token. A revoked token requires reconnecting with another legacy token.
  • publications needs the Medium user id. It uses integration.internal_id, which authenticate sets from data.id; it does not use the channel database id. If a channel was migrated from another system with a different internal id, the call will list the wrong user’s publications or fail.
  • No rate-limit mapping. handle_errors() returns None; generic HTTP handling retries 429/500 up to three times and treats 401 as a refresh request. Medium does not publish a sandbox or a detailed rate-limit table for this legacy API.

References