Dev.to (devto)
Dev.to (Forem) is an article provider: the post body is Markdown and the
publish call creates an article with published: true. Rustiz talks to the
Forem API with a personal API key. There is no developer app, no OAuth
exchange, no scope model and no app review.
- Rust source:
crates/rustiz-providers/src/devto.rs - Upstream port:
dev.to.provider.ts - Editor: Markdown
- Connection mode:
customFields
Overview
| Property | Value |
|---|---|
| Identifier | devto |
| Display name | Dev.to |
| Connection mode | customFields (API key collected by the connect form / MCP) |
| 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; post() 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 article body |
| API version | Forem API v1 endpoints (/api/...), api-key header |
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. There is nothing to
register, so rustiz providers --status reports devto as configured even
before any user has supplied an API key.
| Variable | Required | Default | Purpose |
|---|---|---|---|
DEVTO_BASE_URL | No | https://dev.to | Overrides the API origin. The value is trimmed of a trailing slash. Useful for a Forem instance or a mock server. Example: https://dev.to. |
RESTRICT_UPLOAD_DOMAINS | No | unset | If set, main_image.path must contain this substring. This is a deployment policy, not a Dev.to requirement. |
required_env() purposefully does not include DEVTO_BASE_URL; it is only an
optional override read through Config::base_url("DEVTO", ...).
Step-by-Step Developer App Setup
There is no developer portal application to create.
-
Sign in to the Dev.to account that should publish the articles.
-
Open Settings → Extensions: https://dev.to/settings/extensions.
-
Click Generate API Key (the UI may call it Generate New Key).
-
Copy the key. Treat it as a password: it can create, update and unpublish articles on that account. There are no scopes to select.
-
Optionally verify it before connecting:
curl -s https://dev.to/api/users/me \ -H "api-key: $DEVTO_API_KEY"The response contains
id,name,usernameandprofile_image, which are exactly the fields Rustiz stores duringauthenticate.
Products and permissions
- No products or APIs must be enabled.
- No OAuth scopes exist for a personal API key.
- The key is account-wide. There is no per-publication key.
- To publish under an organization, the account must already be a member of that organization and the organization id must be supplied in the post settings.
Sandbox / testing mode
Dev.to has no sandbox environment. The API acts on the production account.
- Use a secondary Dev.to account for tests, or accept that a test article is public.
- Rustiz always sends
published: true; there is no draft mode in this provider. The provider’s “schedule” is Rustiz waiting until the scheduled time before calling Dev.to. - A useful safe check is
GET /api/users/me, which creates nothing.
Production app review
There is no app review. The same key works in production immediately. Dev.to’s general terms and per-endpoint rate limits still apply.
Connecting a Channel (CLI & MCP)
The credential payload has exactly one key:
| Key | Type | Validation regex | Notes |
|---|---|---|---|
apiKey | password | /^.{3,}$/ | Dev.to personal API key |
CLI
Create a JSON file (or pipe JSON on stdin with --fields -):
{ "apiKey": "dv_xxxxxxxxxxxxxxxxxxxxxxxx" }
rustiz channels connect devto --fields devto-key.json
# or without a file
printf '%s' '{"apiKey":"dv_xxxxxxxxxxxxxxxxxxxxxxxx"}' \
| rustiz channels connect devto --fields -
A successful credential connection is immediate and returns:
{ "connected": true, "pending": false, "name": "Your Dev.to name" }
MCP
connectChannelTool arguments:
{
"provider": "devto",
"customFields": {
"apiKey": "dv_xxxxxxxxxxxxxxxxxxxxxxxx"
}
}
There is no follow-up step. Call integrationList to get the channel id. The
returned connection object is {"type":"customFields","headless":true,...}
in integrationSchema. Use --fields / customFields; --external-url /
externalUrl is not the credential path for this provider.
Provider functions (triggerTool)
| Function | Parameters | Returns | Notes |
|---|---|---|---|
tags | none | [{"value": <number>, "label": "<name>"}] | Calls GET /api/tags?per_page=1000&page=1 with the stored key. value is the numeric Dev.to tag id. |
organizations | none | [{"id": <number>, "name": "...", "username": "..."}] | Reads up to 1000 of the user’s articles from /api/articles/me/all, collects distinct organization.username, then calls /api/organizations/{username} once per organization. |
MCP examples:
{ "integrationId": "int_...", "methodName": "tags" }
{ "integrationId": "int_...", "methodName": "organizations" }
Both functions have 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. Also checked by validate_settings. |
main_image | object | No | Cover image. Properties: id (string, required by the schema), path (string, required), alt (string), thumbnail (string URL). additionalProperties: true. |
canonical | string | No | Canonical URL sent as canonical_url. If it contains (post: it is skipped by URL validation. |
organization | string | No | Dev.to organization id sent as organization_id. Leave empty to publish as the user. |
tags | array | Yes (schema) | maxItems: 4; each item needs value (number, tag id) and label (string, tag name). Only label is sent to Dev.to. |
The schema marks title and tags as required. validate_settings is more
lenient about a missing tags value (it treats it as an empty list), but the
schema and normal editor flow require it.
Example object form (public API / CLI --json):
{
"title": "Shipping a Rust scheduler",
"main_image": { "id": "m1", "path": "https://cdn.example/cover.png" },
"canonical": "https://blog.example/shipping-rust-scheduler",
"organization": "1234",
"tags": [
{ "value": 1, "label": "rust" },
{ "value": 2, "label": "api" }
]
}
MCP settings is an array of {key, value} pairs:
{
"settings": [
{ "key": "title", "value": "Shipping a Rust scheduler" },
{ "key": "tags", "value": [
{ "value": 1, "label": "rust" },
{ "value": 2, "label": "api" }
] }
]
}
The engine adds "__type": "devto" automatically if it is missing.
validate_settings() rules
titlemust exist and be a string; at least 2 Unicode scalar values.canonical, if present and a string, must be a valid URL unless it contains the internal(post:placeholder. The Rust URL check accepts an empty string, anhttp(s)://URL, or awww.host with a dot and a non-space tail.organization, if present and notnull, must be a string.tags, if present and notnull, must be an array with at most 4 items. Each item must be an object with a numericvalueand a stringlabel.main_image, if present and notnull:- must be an object;
idmust be a string;pathmust be a string;- if
RESTRICT_UPLOAD_DOMAINSis set,pathmust contain that value; - the path (query string stripped) must end in
.png,.jpg,.jpeg,.gif,.webpor.mp4(case-insensitive); alt, if present, must be a string;thumbnail, if present, must be a non-empty valid URL.
Media requirements
- Dev.to only receives a cover image URL in
main_image.path; themedia/attachmentsarray on the post is ignored. - The cover is not uploaded by Rustiz. It must already be a public URL.
- The extension check allows
.mp4, but Dev.to’smain_imageis a cover image; a video URL will almost certainly be rejected by the platform. - Body images are whatever Markdown the post contains (Dev.to side-loads external images referenced in the article).
check_validity() is the trait default (Ok(None)). Dev.to has no additional
media-count, dimension or file-type rule in this provider; the only media
validation is the main_image extension/domain check in
validate_settings().
Plugs
No @Plug or @PostPlug methods are implemented. run_plug() and
run_post_plug() use the trait defaults and return
ProviderError::Unsupported.
Implementation Notes & Gotchas
- The article is always public.
post()hard-codespublished: true. There is no draft or native scheduling option. A scheduled post is only delayed by Rustiz; once the publish job runs, the article is live. - No
Acceptheader. Forem’s v1 documentation says v1 requests should sendAccept: application/vnd.forem.api-v1+json. Rustiz sends onlyapi-key. The tests and ordinaryapi-keyendpoints tolerate this, but if Dev.to returns an unexpected response or HTML, missing version negotiation is the first thing to check. - Organization id is sent as a JSON string. The schema and validation
require
organizationto be a string, andpost()sends"organization_id": "<string>". Dev.to’s article schema documentsorganization_idas an integer. If Dev.to rejects the organization, this type mismatch is the cause. Omittingorganizationavoids it. (post:canonical placeholder is only skipped in validation. If a caller passes a string containing(post:,validate_settingsaccepts it, butpost()still sends the non-empty value ascanonical_url. Only omit the field to leave the canonical URL out.- Tag ids are replaced by labels. The tool returns
{value: <id>, label: <name>}, but publishing sends onlylabel. Renaming a tag in the editor changes what is sent; the numeric id is not used by Dev.to on create. tags[].valuemust be a number in Rustiz.docs.postiz.comshows string values in some examples. The Rust validator explicitly requiresValue::is_number; string tag values fail witheach tag needs a numeric \value` (tag id)`.organizationsis an N+1 call and capped at 1000 articles. It reads/api/articles/me/all?per_page=1000; organizations that never appear on those articles are not listed. Large accounts can be slow.- No refresh.
authenticatestores the key with a 100-yearexpires_inandrefresh_token: Some("").refresh_token()returns an empty access token withexpires_in: 0. A genuine 401 therefore cannot be repaired by refresh; reconnect the channel with a fresh key. - 401 maps to refresh. The HTTP layer treats an unclassified 401 as
RefreshToken(testunauthorized_is_a_refresh), so a revoked key shows up as a refresh/reconnect problem. - Canonical uniqueness. A 422 body containing
Canonical url has already been takenis converted to the actionable messageCanonical URL already exists. - Max concurrency 3. The provider limits itself to three simultaneous jobs. Dev.to also enforces its own per-endpoint limits (article creation is commonly limited to 10 requests per 30 seconds); the generic HTTP layer retries 429/500 up to three times with the configured 5-second delay.
References
- Forem API overview: https://developers.forem.com/api
- Forem API v1 reference: https://developers.forem.com/api/v1
- Dev.to API key page: https://dev.to/settings/extensions
- Postiz provider settings: https://docs.postiz.com/public-api/providers/devto
- Postiz posting rules: https://docs.postiz.com/general/platforms/overview