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

Dribbble (dribbble)

Dribbble is an OAuth 2.0 provider for publishing a single shot image. The API is a single-shot multipart upload: one image is posted to POST https://api.dribbble.com/v2/shots, and the new shot id is read from the Location response header. Dribbble creates shots asynchronously, but the Rustiz port marks the post Completed as soon as the API accepts the upload.

  • Rust source: crates/rustiz-providers/src/dribbble.rs
  • Upstream port: dribbble.provider.ts
  • Editor: Normal (plain text)
  • Connection mode: oauth

Read the gotchas before using this provider. The refresh path is a faithful port of an upstream copy/paste bug that calls Pinterest with Pinterest credentials and a Pinterest redirect URI. The team setting is validated but never sent. See Implementation Notes & Gotchas.

Overview

PropertyValue
Identifierdribbble
Display nameDribbble
Connection modeoauth (authorization code; no refresh token in practice)
Editornormal
Channel kindsocial
Comments / threadsNo. supports_comments = false; only the first PostDetails is published
Pending / async publishingNo. supports_pending = false; Dribbble’s 202 + Location is treated as completed immediately
Analyticssupports_analytics = true, but analytics() returns an empty array. The flag advertises the capability; there is no actual data.
Post analyticssupports_post_analytics = true, but post_analytics() returns an empty array.
MentionsNo. supports_mentions = false
is_between_stepsfalse
convert_to_jpegfalse
max_concurrent_job3
Character limit (max_length)40000 characters for the shot description/body
APIDribbble API v2

Server Environment Variables

required_env() returns exactly two variables:

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

required_env_alternatives() is not overridden. provider_status will refuse to generate a login URL if either is empty.

VariableRequiredDefaultPurpose
DRIBBBLE_CLIENT_IDYesnoneOAuth application client id.
DRIBBBLE_CLIENT_SECRETYesnoneOAuth application client secret.
FRONTEND_URLYes, in practicehttp://localhost:5000Builds the OAuth redirect URI. Must be the public origin Dribbble can reach; a trailing slash is stripped.
DRIBBBLE_OAUTH_BASE_URLNohttps://dribbble.comOverrides the authorize/token origin. The paths appended are /oauth/authorize and /oauth/token.
DRIBBBLE_BASE_URLNohttps://api.dribbble.comOverrides the API origin. Paths appended are /v2/user and /v2/shots.
PINTEREST_BASE_URLNohttps://api-sandbox.pinterest.comOnly used by the broken refresh path. It does not affect normal publishing.
PINTEREST_CLIENT_IDNononeRead only by the broken refresh path; not declared in required_env().
PINTEREST_CLIENT_SECRETNononeRead only by the broken refresh path; not declared in required_env().

required_env() deliberately omits PINTEREST_*, even though refresh_token() reads them. That means rustiz providers --status says Dribbble is configured while refresh support is silently incomplete.

Step-by-Step Developer App Setup

  1. Sign in to Dribbble and open https://dribbble.com/account/applications/new.

  2. Fill in:

    • Name — for example Rustiz.

    • Description — required by the form.

    • Website — your product or instance URL.

    • Callback URL / Redirect URI — enter exactly:

      {FRONTEND_URL}/integrations/social/dribbble
      

      For example https://social.example.com/integrations/social/dribbble. Do not add a trailing slash.

  3. Save the application and copy the Client ID and Client Secret. The Postiz Dribbble guide says both are 64 characters long.

  4. Set them on the server:

    DRIBBBLE_CLIENT_ID="your-client-id"
    DRIBBBLE_CLIENT_SECRET="your-client-secret"
    FRONTEND_URL="https://social.example.com"
    
  5. Restart Rustiz and check:

    rustiz providers --status | jq '.[] | select(.identifier=="dribbble")'
    

    configured should be true with no missing variables.

Spelling warning. The Postiz documentation page for Dribbble currently shows DRIBBLE_CLIENT_ID / DRIBBLE_CLIENT_SECRET (two bs, not three) and a redirect path /integrations/social/dribble (one fewer b). Those are typos. This Rust port uses DRIBBBLE_CLIENT_ID / DRIBBBLE_CLIENT_SECRET and the correct /integrations/social/dribbble path. Register the URI the Rust code actually builds, not the one on that docs page.

Products and APIs

Dribbble has no products or API-enablement console. Registering an OAuth application is enough. The scopes are requested in the authorization URL.

Exact OAuth scopes

scopes() returns:

#![allow(unused)]
fn main() {
vec!["public", "upload"]
}

The authorization URL sends scope=public+upload (a space-separated list, encoded as +). authenticate() calls check_scopes(&["public", "upload"], token["scope"]); the token response must contain both scopes. upload is the one that permits POST /v2/shots; public is read-only public information.

Redirect URI details

The exact URI is:

{FRONTEND_URL (trailing slash trimmed)}/integrations/social/dribbble

Examples:

DeploymentRedirect URI to register
Production https://social.example.comhttps://social.example.com/integrations/social/dribbble
Local default FRONTEND_URLhttp://localhost:5000/integrations/social/dribbble
A local frontend on port 4200http://localhost:4200/integrations/social/dribbble
A Docker frontend on port 5000http://localhost:5000/integrations/social/dribbble

Dribbble’s own OAuth docs say the redirect host and port must exactly match the registered callback; if redirect_uri is omitted, the registered callback is used. Rustiz always sends it, both at authorize time (URL-encoded) and at token exchange (unencoded; see gotchas). There is no redirectmeto.com relay in this provider: the value is built directly from FRONTEND_URL, so an HTTP local frontend registers an HTTP URI and an HTTPS production frontend registers an HTTPS URI.

Sandbox / testing mode

Dribbble has no sandbox environment and no separate test app mode.

  • The app is usable immediately after registration; there is no documented review/approval queue.
  • Use a secondary Dribbble account for tests. A shot created through the API is public; there is no draft status in the create-shot API.
  • scheduled_for exists in Dribbble’s API but requires a Pro account, team membership or a team; Rustiz does not expose it. The scheduler controls when Rustiz calls the API.
  • Rate limits are 60 requests per minute and 1,440 per day per authenticated user. Watch X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 is returned when exceeded.
  • The application can be suspended for abuse, spam or API misuse. That is a platform enforcement action, not a pre-publication review.

Production app review

No separate app-review step is documented in the OAuth flow: registration is self-serve and the app can authorize users immediately. Dribbble’s API terms reserve the right to refuse API access at any time and to throttle or block a service, and an app can be suspended for abuse or API misuse. For a public or commercial integration, treat access as discretionary and read the API terms before launch.

Connecting a Channel (CLI & MCP)

CLI

# Opens the Dribbble authorization page in the default browser.
rustiz channels connect dribbble --open

# Without --open: the URL is printed to stderr and the command waits.
rustiz channels connect dribbble

# Remote server: open the URL yourself, then poll.
rustiz channels connect-status <connectionId>

In local/in-process mode the CLI serves the callback on the port of FRONTEND_URL (if it is this machine) and waits for the new channel. On success it prints the channel JSON.

MCP

Start the OAuth flow:

{ "provider": "dribbble" }

The tool returns either a URL elicitation (for clients that support it) or:

{
  "connected": false,
  "pending": true,
  "connectionId": "abc123",
  "url": "https://dribbble.com/oauth/authorize?...",
  "message": "A human must open this URL and log in. Then poll with connectionId or check integrationList."
}

Give the URL to a human, then poll:

{ "connectionId": "abc123" }

There is no follow-up step: is_between_steps is false. Call integrationList for the channel id.

Provider functions (triggerTool)

FunctionParametersReturnsNotes
teamsnone[{"id": <number>, "name": "..."}]Calls GET /v2/user and maps teams. If the user has no teams, returns [].

MCP:

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

The function has an empty input_schema / dataSchema.

The teams result cannot currently be used for posting. The team setting is ignored by post(), and Dribbble requires a numeric team_id field in the shot upload, which the provider never sends. Treat teams as informational only.

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: 1; must be a non-empty string. No maximum is enforced by Rustiz. This is Dribbble’s required shot title.
teamstringNoDescribed as a team URL/id. validate_settings requires a valid URL when the value is present and non-null. It is never read by post(). An empty string fails validation; omit the field or use null to post on the personal profile.

There is no description, tags, low_profile, rebound_source_id or scheduled_for setting. The post body (PostDetails.message) becomes the shot description.

Example object form:

{
  "title": "Dashboard UI — dark mode",
  "team": null
}

MCP settings array:

{
  "settings": [
    { "key": "title", "value": "Dashboard UI — dark mode" }
  ]
}

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

validate_settings() rules

  1. title must exist and be a string; it must not be empty.
  2. team, if present and not null, must be a string and must parse as a URL.
    • An empty string is not a URL and fails with team must be a URL. This matters because docs.postiz.com examples use "team": "".
    • Use null or omit team for a personal shot.
  3. There is no check that the team URL belongs to the authenticated user, and no conversion from a team URL to Dribbble’s numeric team_id.

check_validity() rules

check_validity() is called before scheduling/publishing with each part’s media. It looks only at the first post part:

  1. If any item’s path contains "mp4" (case-sensitive), return Does not support mp4 files.
  2. Otherwise, the part must have exactly one media item. Zero or two or more returns Requires one item.
  3. It reads the image dimensions and accepts only:
    • 400×300, or
    • 800×600. Anything else returns Invalid image size. Requires 400x300 or 800x600 px images.

Because the MP4 check runs before the count check, a two-item list containing an MP4 returns the MP4 message, not Requires one item.

The validation does not check:

  • file size (Dribbble’s limit is 8 MB);
  • actual file type (Dribbble accepts GIF, JPG or PNG);
  • file extension;
  • whether the image is animated or has the right color profile.

A 400×300 WebP or a 400×300 20 MB PNG passes Rustiz validation and is then rejected by Dribbble’s API.

Media and publish behavior

  • Exactly one media item is used: first.media.first(). All other media is ignored.
  • The file is streamed as multipart form data under the field name image, with the original filename and a MIME type inferred from the extension (jpg/jpeg, png, gif, webp, mp4; unknown → no MIME part).
  • The form also sends:
    • title from settings.title;
    • description from first.message.
  • It does not send team_id, tags, low_profile, rebound_source_id or scheduled_for.
  • Dribbble’s API responds 202 Accepted with a Location header such as https://api.dribbble.com/v2/shots/471756. Rustiz treats any 2xx as success, takes the last path segment as the post id, and returns https://dribbble.com/shots/{id}. The shot may return 404 at that URL until Dribbble finishes processing, because creation is asynchronous.
  • If the 2xx response has no Location, the post id is empty and the release URL becomes https://dribbble.com/shots/ — a silent bad success.
  • A 4xx other than 429 is converted to a bad_body error using the platform message field, or Dribbble rejected the shot with status {status} if there is no message. 429 is passed to the generic retry path.

Plugs

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

Implementation Notes & Gotchas

The refresh path calls Pinterest (upstream bug, faithfully ported)

This is the most important thing to know about dribbble.rs. The module header says it explicitly:

Faithful port note: upstream refreshToken calls the Pinterest sandbox endpoints with PINTEREST_CLIENT_ID / PINTEREST_CLIENT_SECRET. That copy paste bug is kept here.

In refresh_token() (around lines 258–318):

  1. It builds Basic auth from PINTEREST_CLIENT_ID and PINTEREST_CLIENT_SECRET.

  2. It posts to {PINTEREST_BASE_URL}/v5/oauth/token, default https://api-sandbox.pinterest.com/v5/oauth/token.

  3. The form contains grant_type=refresh_token, the Dribbble refresh token, scope=public,upload, and a redirect_uri of:

    {FRONTEND_URL}/integrations/social/pinterest
    

    That is pinterest, not dribbble — line 283 of dribbble.rs.

  4. It then calls {PINTEREST_BASE_URL}/v5/user_account with the returned token and returns a AuthTokenDetails whose id, name, username and picture are Pinterest account fields.

Consequences:

  • A Dribbble refresh will fail unless Pinterest sandbox credentials are also configured, and it will send the Dribbble refresh token to Pinterest.
  • Even if it succeeds, the token is a Pinterest token and the returned identity is a Pinterest user; it cannot be used to post to Dribbble.
  • PINTEREST_CLIENT_ID / PINTEREST_CLIENT_SECRET are not declared in required_env(), so the deployment’s provider status will not warn about them.
  • Normal Dribbble operation tries to avoid refresh: authenticate() sets expires_in: Some(999999999) (about 31.7 years) and refresh_token: Some(""). Dribbble’s token response does not document a refresh token. A 401 still enters the refresh path, where this bug fires.
  • Workaround: do not rely on token refresh. If a Dribbble channel is marked refresh-needed or starts returning 401, reconnect it through rustiz channels connect dribbble rather than trying to repair the token.

Other gotchas

  • The team setting is dead code. It is in the schema and validated, but post() never reads it. Dribbble’s create-shot API wants a numeric team_id; the teams tool returns numeric ids, but there is no path from the setting to the request. Team shots cannot be published by this provider.

  • "team": "" fails validation. The docs examples use an empty string, but is_url("") is false, so the call fails with team must be a URL. Omit the key or send null.

  • The token exchange query is hand-built and unencoded. authenticate() constructs:

    POST {DRIBBBLE_OAUTH_BASE_URL}/oauth/token?client_id=...&client_secret=...&code=...&redirect_uri=...
    

    The authorization code and redirect URI are not URL-encoded. Dribbble authorization codes are normally URL-safe and the redirect URI has no query string, so this usually works, but a client secret or code containing &, # or + would break the request.

  • expires_in is synthetic. Dribbble’s OAuth token response contains access_token, token_type and scope, but no expires_in. Rustiz stores 999999999 seconds so the engine will not try to refresh. This is a deliberate workaround, not data from Dribbble.

  • No refresh token is stored. authenticate() sets refresh_token: Some(""). Any engine refresh attempt passes an empty string to the Pinterest bug. There is no way to recover a Dribbble token through this provider.

  • analytics flags are misleading. info.supports_analytics and info.supports_post_analytics are true, so generic callers may ask for analytics, but both methods return Ok(vec![]). Dribbble does not expose detailed shot analytics through this API.

  • Creation is asynchronous. Dribbble returns 202 + Location; the shot may 404 for a short time while it is processed. Rustiz marks the post Completed immediately. A later analytics/post lookup at the returned https://dribbble.com/shots/{id} may briefly fail.

  • Missing Location is a silent empty success. The code does not treat a 2xx without Location as an error; it returns an empty post id and https://dribbble.com/shots/. Check the response if a Dribbble post appears successful with no URL.

  • Rate limits are per authenticated user. 60 requests/minute and 1,440/day. max_concurrent_job = 3 keeps concurrency low, but retries and multiple channels on one account share the limit. 429 is retried by the generic HTTP layer up to three times; there is no provider-specific handle_errors() mapping.

  • Validation is dimensions-only. See the media section: size, MIME and extension are unchecked. Use 800×600 PNG/JPG under 8 MB for the most reliable result.

  • Description is plain text. The editor is normal, so HTML is stripped before message reaches Dribbble. Tags are not sent; put hashtags in the description if desired.

  • The release URL is constructed, not read from Dribbble. It is always https://dribbble.com/shots/{id}; Dribbble’s html_url field is not used.

References