multica-ai/multica · error · ErrBindingNotWorkspaceMember

slack: redeemer is not a workspace member

Error message

slack: redeemer is not a workspace member

What it means

Slack binding sentinel error: the user redeeming the binding token is not a member of the token's workspace. Detected by an explicit membership check during redemption; the HTTP boundary maps it to 403.

Source

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

// 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
}

// BindingTokenService mints and redeems Slack binding tokens. Redemption is
// transactional: consuming the token and inserting the channel_user_binding row

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Have a workspace admin re-add the redeeming user, then request a fresh binding link (the old one may have expired).
  2. Make sure the redeem page session matches a user account that is a member of the token's workspace.
  3. Check membership rows for the (workspace, user) pair if the user believes they are a member — stale session data can mask a removal.

Example fix

// before
res, err := svc.Redeem(ctx, rawToken)
if err != nil {
	http.Error(w, err.Error(), 500)
}

// after
res, err := svc.Redeem(ctx, rawToken)
if errors.Is(err, slack.ErrBindingNotWorkspaceMember) {
	http.Error(w, "you must be a member of this workspace", http.StatusForbidden)
	return
}
Defensive patterns

Strategy: validation

Validate before calling

if !members.IsMember(ctx, token.WorkspaceID, user.ID) {
	return respondForbidden(w, "join the workspace before linking Slack")
}
_ = slackSvc.Redeem(ctx, rawToken)

Try / catch

if err := slackSvc.Redeem(ctx, rawToken); err != nil {
	if errors.Is(err, slack.ErrBindingNotWorkspaceMember) {
		return respondStatus(w, http.StatusForbidden)
	}
	return err
}

Prevention

When it happens

Trigger: Redeeming a Slack binding token when the redeemer has no membership row in the workspace the token was minted for — e.g. removed from the workspace mid-flow, or signed into a different Multica account that never belonged to that workspace.

Common situations: Admin kicks the user during the token's 15-minute life and the user then clicks the DM link; multi-workspace users redeeming a link while the wrong workspace/account is active in the session.

Related errors


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