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
mediumprovider cannot be used. See https://help.medium.com/hc/en-us/articles/213480228-API-Importing.
Overview
| Property | Value |
|---|---|
| Identifier | medium |
| Display name | Medium |
| Connection mode | customFields (legacy integration token) |
| Editor | markdown |
| Channel kind | social (article content) |
| Comments / threads | No. supports_comments = false; only the first PostDetails is published |
| Pending / async publishing | No. supports_pending = false; returns PostStatus::Completed |
| Analytics | No. supports_analytics = false |
| Post analytics | No. supports_post_analytics = false |
| Mentions | No. supports_mentions = false |
is_between_steps | false |
convert_to_jpeg | false |
max_concurrent_job | 3 |
Character limit (max_length) | 100000 characters for the story body |
| API | Medium 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.
| Variable | Required | Default | Purpose |
|---|---|---|---|
MEDIUM_BASE_URL | No | https://api.medium.com | Overrides 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.
-
Sign in to Medium.
-
Open Settings → Security and apps → Integration tokens (https://medium.com/me/settings). Medium’s own API documentation calls the section Integration Tokens.
-
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. -
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,publishPostand optionallyuploadImage, 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"wheneversettings.publicationis set. A draft in a publication stays pending until an editor publishes it. - A post without
publicationusespublishStatus: "public"and is live immediately. GET /v1/meandGET /v1/users/{id}/publicationsare 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:
| Key | Type | Validation regex | Notes |
|---|---|---|---|
apiKey | password | /^.{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)
| Function | Parameters | Returns | Notes |
|---|---|---|---|
publications | none | The data array from GET /v1/users/{id}/publications | Uses 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. Userustiz posts create --json post.jsonwith aCreatePostDto(type,date,posts[].settings, …) when any of these fields are required.
| Field | Type | Required | Validation / behavior |
|---|---|---|---|
title | string | Yes | minLength: 2; checked. Medium itself ignores titles longer than 100 characters and synthesizes one from the content. |
subtitle | string | Yes | minLength: 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. |
canonical | string | No | Sent as canonicalUrl. If it contains (post:, URL validation is skipped. |
publication | string | No | Medium 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". |
tags | array | No | maxItems: 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
titlemust exist, be a string and have at least 2 characters.subtitlemust exist, be a string and have at least 2 characters.canonical, if present and a string, must be a valid URL unless it contains(post:.publication, if present and notnull, must be a string. An empty string is accepted and means “publish on the profile”.tags, if present and notnull, must be an array of at most 4 items. Each item must be an object with a stringvalueand a stringlabel. 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_imagefield in the schema. Medium is not sent a cover image and Rustiz does not call Medium’s image upload endpoint. - The post
media/attachmentsarray 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()setspublishStatus: "draft"wheneverpublicationis 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 nounlistedsetting 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.
labelis not sent;valueis. - 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, butpost()sends any non-emptycanonicalstring ascanonicalUrl. Omit the field to leave the canonical URL out.- No refresh. Tokens are stored with a 100-year
expires_inand an empty refresh token;refresh_token()returns an empty access token. A revoked token requires reconnecting with another legacy token. publicationsneeds the Medium user id. It usesintegration.internal_id, whichauthenticatesets fromdata.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()returnsNone; 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
- Medium API/importing status (no new integration tokens): https://help.medium.com/hc/en-us/articles/213480228-API-Importing
- Medium API documentation (archived, self-issued tokens, publication rules, no sandbox): https://github.com/Medium/medium-api-docs
- Medium account settings: https://medium.com/me/settings
- Postiz provider settings: https://docs.postiz.com/public-api/providers/medium
- Postiz posting rules: https://docs.postiz.com/general/platforms/overview