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
- Ensure an inbound activity from the conversation was accepted first, so paperclipRecordAcceptedActivity persisted the serviceUrl.
- Increase the accepted-activity cache TTL or re-persist the route if it expires before file sends.
- Verify the durable state store (teams.chat.getState()) is the same one used when recording the route — a swapped store loses the key.
- For legacy thread ids with embedded serviceUrl, confirm decodeThreadId still returns serviceUrl; otherwise migrate ids.
- 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
- Only send proactive file cards to conversations that have delivered at least one accepted inbound activity.
- Persist the route durably (not just TTL cache) if file sends can happen long after the last inbound message.
- Use one state store consistently between recordAcceptedActivity and withThreadServiceUrl.
- Alert on route-expiry: log when a required-route lookup misses so the gap is visible.
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
- Teams destination is missing its conversation identity
- invalid file-card shape
- Teams file cards require an exact personal conversation
- Image exceeds attachment bound
- Invalid Teams private binding
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/86b47c8b5ed6e959.
Report an issue: GitHub.