paperclipai/paperclip · error · TeamsServiceUrlValidationError

Teams file destination is missing its verified route

Error message

Teams file destination is missing its verified route

What it means

When requireRoute is true, withThreadServiceUrl demands a verified route before any outbound file operation: either a persisted service URL in chat state for the conversation, or a serviceUrl embedded in the decoded thread id. Microsoft requires replies to go to the authenticated serviceUrl, so sending a file card without a verified route is refused with TeamsServiceUrlValidationError.

Source

Thrown at server/src/services/chat-sdk-runtime.ts:1207

    threadId: string,
    operation: () => Promise<T>,
    requireRoute = false,
  ): Promise<T> => {
    const decoded = teams.decodeThreadId!(threadId);
    if (typeof decoded.conversationId !== "string" || !decoded.conversationId) {
      throw new TeamsServiceUrlValidationError(
        "Teams destination is missing its conversation identity",
      );
    }
    const persistedServiceUrl = await teams.chat
      ?.getState()
      .get(teamsConversationRouteStateKey(decoded.conversationId));
    if (
      requireRoute &&
      persistedServiceUrl == null &&
      decoded.serviceUrl == null
    ) {
      throw new TeamsServiceUrlValidationError(
        "Teams file destination is missing its verified route",
      );
    }
    return await withServiceUrl(
      persistedServiceUrl ?? decoded.serviceUrl ?? defaultApi.serviceUrl,
      operation,
    );
  };

  if (enableFileConsent) {
    if (
      typeof teams.app.send !== "function" ||
      typeof teams.app.on !== "function"
    ) {
      throw new TeamsAdapterCompatibilityError(
        "file-consent App hooks are unavailable",
      );
    }

View on GitHub (pinned to 01ad858492)

Solutions

  1. Ensure an inbound activity from the conversation was accepted first, so paperclipRecordAcceptedActivity persisted the serviceUrl.
  2. Increase the accepted-activity cache TTL or re-persist the route if it expires before file sends.
  3. Verify the durable state store (teams.chat.getState()) is the same one used when recording the route — a swapped store loses the key.
  4. For legacy thread ids with embedded serviceUrl, confirm decodeThreadId still returns serviceUrl; otherwise migrate ids.
  5. Check that trustedTeamsServiceUrl is not rejecting the persisted URL earlier and leaving the route unset.

Example fix

// before
await teams.paperclipSendFileCard(threadId, 'consent', input); // no route yet
// after
const decoded = teams.decodeThreadId(threadId);
if (!decoded.serviceUrl && !(await state.get(teamsConversationRouteStateKey(decoded.conversationId)))) {
  throw new Error('route not verified; wait for inbound activity');
}
await teams.paperclipSendFileCard(threadId, 'consent', input);
Defensive patterns

Strategy: validation

Validate before calling

const decoded = teams.decodeThreadId(threadId);
const route = await teams.chat.getState().get(teamsConversationRouteStateKey(decoded.conversationId));
if (route == null && decoded.serviceUrl == null) {
  throw new Error('No verified Teams route for this conversation; wait for an inbound activity first');
}

Try / catch

try {
  await teams.paperclipSendFileCard(threadId, kind, input);
} catch (err) {
  if (err instanceof TeamsServiceUrlValidationError && err.message.includes('verified route')) {
    logger.warn(`No verified route for ${threadId}; deferring file send`);
    await deferFileSend(threadId, kind, input); // queue until an inbound activity records the route
  }
}

Prevention

When it happens

Trigger: Sending a file-consent/file_info card via paperclipSendFileCard for a conversation where teams.chat state has no teamsConversationRouteStateKey entry and decoded.serviceUrl is null — typically before any inbound activity recorded the route, or after the persisted route expired (TTL).

Common situations: Proactively messaging a conversation the bot never received an activity from; state TTL expired between inbound message and file send; state store cleared or running against a fresh database while replaying old thread ids.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/86b47c8b5ed6e959. Report an issue: GitHub.