multica-ai/multica · error · ErrBindingAlreadyAssigned

slack: user id is already bound to a different user

Error message

slack: user id is already bound to a different user

What it means

Slack binding sentinel error: the Slack user id being bound is already bound to a different Multica user. As with lark, a binding token cannot transfer an already-bound Slack identity; account transfer must go through an explicit unbind flow.

Source

Thrown at server/internal/integrations/slack/binding.go:37

// This file is the Slack user-binding token flow: an unbound Slack user who
// messages the bot gets a "link your account" prompt (minted here, delivered by
// the OutboundReplier), clicks through to the in-product redeem page, and their
// Slack user id is bound to their Multica account. It mirrors
// lark.BindingTokenService but runs on the generic channel_* queries with
// channel_type='slack' (lark's ChannelStore hardcodes 'feishu').

// BindingTokenTTL bounds a token's life. The channel_binding_token CHECK
// enforces the same 15-minute cap so a misconfigured caller cannot mint longer.
const BindingTokenTTL = 15 * time.Minute

var (
	// ErrBindingTokenInvalid: token unknown / already consumed / expired. One
	// opaque error for all three avoids a replay timing oracle.
	ErrBindingTokenInvalid = errors.New("slack: binding token invalid or expired")
	// ErrBindingAlreadyAssigned: this Slack user id is already bound to a
	// different Multica user (account transfer must go through explicit unbind).
	ErrBindingAlreadyAssigned = errors.New("slack: user id is already bound to a different user")
	// ErrBindingNotWorkspaceMember: the redeemer is not a member of the token's
	// workspace. Translated to 403 at the HTTP boundary.
	ErrBindingNotWorkspaceMember = errors.New("slack: redeemer is not a workspace member")
)

// BindingToken is a freshly minted token. The raw value is returned exactly
// once (embedded in the binding URL); only its hash is persisted.
type BindingToken struct {
	Raw       string
	ExpiresAt time.Time
}

// RedeemedBindingToken is returned after a successful redemption.
type RedeemedBindingToken struct {
	WorkspaceID    pgtype.UUID
	InstallationID pgtype.UUID
	SlackUserID    string
}

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Sign in as the Multica user that currently owns the binding and unbind the Slack user id via the explicit unbind flow, then request a new binding link.
  2. Confirm the intended owner by looking up the existing channel user-binding row for the Slack user id before retrying.
  3. For shared Slack accounts, agree on a single owning Multica account instead of round-robining the binding.

Example fix

// before
res, err := svc.Redeem(ctx, rawToken)
// -> "slack: user id is already bound to a different user"

// after
res, err := svc.Redeem(ctx, rawToken)
if errors.Is(err, slack.ErrBindingAlreadyAssigned) {
	renderUnbindRequired(w) // point user at the unbind flow
	return
}
Defensive patterns

Strategy: try-catch

Validate before calling

owner, err := store.GetChannelUserBinding(ctx, "slack", slackUserID)
if err == nil && owner != redeemingUserID {
	return renderUnbindRequired(w)
}
_ = slackSvc.Redeem(ctx, rawToken)

Try / catch

res, err := slackSvc.Redeem(ctx, rawToken)
if err != nil {
	if errors.Is(err, slack.ErrBindingAlreadyAssigned) {
		return respondConflict(w, "bound to another account — unbind first")
	}
	return err
}

Prevention

When it happens

Trigger: Redeeming a Slack binding token while a channel user-binding row already maps this Slack user id to another Multica user. Happens when the same person has two Multica accounts, or a Slack account is shared and one member already completed the link.

Common situations: User changed Multica accounts (left company A instance, joined with new account) and re-runs binding; shared/team Slack login already bound by a colleague; test environments reusing one Slack user against multiple accounts.

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/0fd8b7ac0e0fff1e. Report an issue: GitHub.