Building Slack and Microsoft Teams Integrations for Real-Time Tender Collaboration

Introduction

The AI Tender Opportunity Scanner can now:

  • Discover public tender opportunities
  • Analyze tenders with AI
  • Match opportunities to business profiles
  • Rank tenders by relevance
  • Manage saved opportunities
  • Track bid pipeline stages
  • Monitor submission deadlines
  • Generate in-app notifications
  • Deliver immediate email alerts
  • Send daily tender opportunity digests

The platform can already notify users through the application and email.

However, many consulting firms, agencies, procurement teams, and technology companies conduct most of their daily collaboration inside Slack or Microsoft Teams.

Bid decisions often happen through conversations such as:

This tender looks relevant. Can someone review it?
Do we have the required security certification?
Who can estimate the implementation effort?
Should we schedule a bid decision meeting?
The deadline is next Friday. Who owns the response?

When tender alerts are delivered directly into collaboration channels, teams can review opportunities and coordinate responses without constantly switching applications.

In this article, we will build integrations for Slack and Microsoft Teams that can:

  • Send new high-match tender alerts
  • Deliver deadline reminders
  • Publish daily opportunity digests
  • Share tender summaries
  • Link directly to opportunity pages
  • Route alerts to selected channels
  • Prevent duplicate messages
  • Track delivery attempts
  • Support interactive actions
  • Prepare the architecture for future two-way collaboration

This extends the AI Tender Opportunity Scanner from an individual productivity tool into a collaborative tender intelligence platform.


Development Progress

Project Scope and Roadmap ████████████████████ 100%
MVP Definition ████████████████████ 100%
Technology Stack ████████████████████ 100%
System Architecture ████████████████████ 100%
Database Design ████████████████████ 100%
Backend Project Structure ████████████████████ 100%
SQLAlchemy Models ████████████████████ 100%
Alembic Migration ████████████████████ 100%
Repository Layer ████████████████████ 100%
CRUD Service Layer ████████████████████ 100%
Tender Ingestion Pipeline ████████████████████ 100%
AI Analysis Pipeline ████████████████████ 100%
AI Matching Engine ████████████████████ 100%
React Dashboard ████████████████████ 100%
Tender Detail Page ████████████████████ 100%
Saved Opportunities Workspace ████████████████████ 100%
Pipeline Analytics Dashboard ████████████████████ 100%
Notifications & Monitoring ████████████████████ 100%
Email Notifications ████████████████████ 100%
Slack & Teams Integrations ████████████████████ 100%
Authentication & Organizations ░░░░░░░░░░░░░░░░░░░░ 0%

What We Are Building

A high-match opportunity notification in Slack may look like this:

AI Tender Opportunity Scanner
New High-Match Tender
Azure Cloud Migration Services
Match Score: 94%
Buyer: City Digital Services
Contract Value: €1,200,000
Deadline: 18 days
Why it matches:
• Azure cloud expertise
• Public-sector experience
• Migration and security capabilities
[View Tender] [Save Opportunity]

A Microsoft Teams deadline reminder may look like:

Tender Deadline Approaching
Cybersecurity Assessment Framework
Submission Deadline:
3 days remaining
Pipeline Stage:
Preparing Bid
Next Action:
Complete pricing review
Priority:
Urgent
[Open Opportunity Workspace]

The integrations will begin as outbound notification channels.

Later, they can support two-way actions such as:

Save Opportunity
Assign Owner
Change Pipeline Stage
Add Internal Note
Schedule Bid Review
Dismiss Tender

Why Integrate Collaboration Platforms?

Email is useful for personal awareness.

Slack and Microsoft Teams are better suited to shared action.

Compare the workflows.

Without integration:

User receives email
|
v
User opens platform
|
v
User copies tender link
|
v
User opens Slack or Teams
|
v
User writes summary
|
v
Team begins discussion

With integration:

Tender matched
|
v
Alert appears in team channel
|
v
Team reviews opportunity
|
v
Discussion begins immediately

The integration reduces friction between discovering an opportunity and making a bid decision.


Integration Architecture

The delivery architecture should remain channel-independent.

Tender Monitoring
|
v
Notification Service
|
v
Delivery Orchestrator
|
+---- In-App Adapter
|
+---- Email Adapter
|
+---- Slack Adapter
|
+---- Teams Adapter
|
+---- Future Webhook Adapter

The notification service decides what happened.

The delivery adapters decide how that information is formatted and delivered.


Separate Events From Delivery Channels

A tender event may be:

New high-match tender

That event can be delivered through several channels:

In-app notification
Email
Slack
Microsoft Teams

Do not create separate business logic for every channel.

Bad architecture:

Deadline checker sends Slack message
Deadline checker sends Teams message
Deadline checker sends email
Deadline checker creates in-app alert

Better architecture:

Deadline checker
|
v
Create notification event
|
v
Delivery orchestrator
|
+---- Slack
+---- Teams
+---- Email
+---- In-App

This separation prevents duplicated logic.


Collaboration Integration Types

There are two common integration models.

Incoming Webhooks

The platform sends messages to a predefined collaboration channel.

Advantages:

  • Simple to implement
  • Suitable for prototypes
  • Limited permissions
  • Fast setup

Limitations:

  • Usually one-directional
  • Limited interaction support
  • Channel selection may be fixed
  • Weak user identity mapping

Installed Application or Bot

The organization installs a Slack or Teams application.

Advantages:

  • Channel discovery
  • Interactive buttons
  • User authentication
  • Direct messages
  • Two-way workflows
  • Richer administration

Limitations:

  • More complex authentication
  • More security requirements
  • App registration and review may be required

For the MVP, we can start with incoming webhook-style delivery while keeping the architecture ready for OAuth-based applications.


Integration Data Model

Create:

app/models/collaboration_integration.py

Recommended fields:

id
organization_id
created_by_user_id
provider
display_name
webhook_url_encrypted
workspace_name
channel_name
channel_id
is_active
created_at
updated_at
last_successful_delivery_at
last_failed_delivery_at
last_error

The provider field supports:

slack
microsoft_teams

Later, it can support:

discord
google_chat
generic_webhook

Provider Enum

from enum import Enum
class CollaborationProvider(str, Enum):
SLACK = "slack"
MICROSOFT_TEAMS = "microsoft_teams"

Integration Status

Add an integration-status enum:

class IntegrationStatus(str, Enum):
ACTIVE = "active"
DISABLED = "disabled"
ERROR = "error"
REVOKED = "revoked"

The status helps distinguish temporary delivery failures from intentionally disabled integrations.


SQLAlchemy Integration Model

from datetime import datetime
from sqlalchemy import (
Boolean,
DateTime,
Enum as SqlEnum,
ForeignKey,
Integer,
String,
Text,
)
from sqlalchemy.orm import Mapped, mapped_column
from app.database.base import Base
class CollaborationIntegration(Base):
__tablename__ = "collaboration_integrations"
id: Mapped[int] = mapped_column(
Integer,
primary_key=True,
)
organization_id: Mapped[int] = mapped_column(
ForeignKey(
"organizations.id",
ondelete="CASCADE",
),
nullable=False,
index=True,
)
created_by_user_id: Mapped[int] = mapped_column(
ForeignKey(
"users.id",
ondelete="CASCADE",
),
nullable=False,
)
provider: Mapped[CollaborationProvider] = mapped_column(
SqlEnum(
CollaborationProvider,
name="collaboration_provider",
),
nullable=False,
index=True,
)
display_name: Mapped[str] = mapped_column(
String(255),
nullable=False,
)
webhook_url_encrypted: Mapped[str] = mapped_column(
Text,
nullable=False,
)
workspace_name: Mapped[str | None] = mapped_column(
String(255),
nullable=True,
)
channel_name: Mapped[str | None] = mapped_column(
String(255),
nullable=True,
)
channel_id: Mapped[str | None] = mapped_column(
String(255),
nullable=True,
)
status: Mapped[IntegrationStatus] = mapped_column(
SqlEnum(
IntegrationStatus,
name="integration_status",
),
default=IntegrationStatus.ACTIVE,
nullable=False,
index=True,
)
is_active: Mapped[bool] = mapped_column(
Boolean,
default=True,
nullable=False,
)
last_successful_delivery_at: Mapped[
datetime | None
] = mapped_column(
DateTime(timezone=True),
nullable=True,
)
last_failed_delivery_at: Mapped[
datetime | None
] = mapped_column(
DateTime(timezone=True),
nullable=True,
)
last_error: Mapped[str | None] = mapped_column(
Text,
nullable=True,
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=datetime.utcnow,
nullable=False,
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=datetime.utcnow,
onupdate=datetime.utcnow,
nullable=False,
)

Why Use Organization-Level Integrations?

Slack workspaces and Microsoft Teams environments normally belong to organizations rather than individual users.

An organization-level integration allows multiple users to send alerts to the same collaboration channel.

Example:

Organization:
Northstar Consulting
Slack Channel:
#tender-opportunities
Users:
Bid Manager
Technical Consultant
Sales Director
Delivery Manager

All authorized members can work from the same opportunity feed.


Protecting Webhook URLs

Webhook URLs are credentials.

Anyone with access to a valid webhook URL may be able to send messages into the connected channel.

Never:

  • Return the full webhook URL through the API
  • Display it after saving
  • Store it in logs
  • Include it in error messages
  • Commit it to source control
  • Store it unencrypted in production

The backend should encrypt the URL before storing it.


Webhook Encryption Service

Create:

app/security/secret_encryption.py

Example interface:

class SecretEncryptionService:
def encrypt(
self,
plaintext: str,
) -> str:
...
def decrypt(
self,
ciphertext: str,
) -> str:
...

Store the encryption key outside the database.

Example environment variable:

INTEGRATION_ENCRYPTION_KEY=...

For production, a managed secrets service or key-management system is preferable.


Creating the Migration

Generate the migration:

alembic revision --autogenerate -m "create collaboration integrations"

Apply it:

alembic upgrade head

Verify that the migration creates:

  • Provider enum
  • Integration-status enum
  • Foreign keys
  • Organization index
  • Provider index
  • Active-status fields
  • Delivery-health fields

Integration Schemas

Create:

app/schemas/collaboration_integration.py

Create request schema:

from pydantic import (
AnyHttpUrl,
BaseModel,
ConfigDict,
Field,
)
class CollaborationIntegrationCreate(
BaseModel
):
provider: CollaborationProvider
display_name: str = Field(
min_length=2,
max_length=255,
)
webhook_url: AnyHttpUrl
workspace_name: str | None = None
channel_name: str | None = None

Create response schema:

class CollaborationIntegrationResponse(
BaseModel
):
model_config = ConfigDict(
from_attributes=True
)
id: int
provider: CollaborationProvider
display_name: str
workspace_name: str | None
channel_name: str | None
status: IntegrationStatus
is_active: bool

Notice that the response does not expose the webhook URL.


Integration Update Schema

class CollaborationIntegrationUpdate(
BaseModel
):
display_name: str | None = Field(
default=None,
min_length=2,
max_length=255,
)
channel_name: str | None = None
is_active: bool | None = None

Provide a separate endpoint for replacing credentials.

This reduces the chance of accidentally exposing or overwriting secret values during routine updates.


Notification Routing Preferences

Not every notification should be sent to every channel.

Create:

collaboration_notification_rules

Recommended fields:

id
integration_id
notification_type
minimum_severity
minimum_match_score
enabled
send_immediately
include_in_digest

Example rules:

New high-match tender:
Send when score >= 85
Deadline warning:
Send when 7 days remain
Deadline urgent:
Always send
Action overdue:
Send
Daily digest:
Send every weekday morning

Rule Model

class CollaborationNotificationRule(Base):
__tablename__ = (
"collaboration_notification_rules"
)
id: Mapped[int] = mapped_column(
Integer,
primary_key=True,
)
integration_id: Mapped[int] = mapped_column(
ForeignKey(
"collaboration_integrations.id",
ondelete="CASCADE",
),
nullable=False,
index=True,
)
notification_type: Mapped[
NotificationType
] = mapped_column(
SqlEnum(
NotificationType,
name="collaboration_notification_type",
),
nullable=False,
)
enabled: Mapped[bool] = mapped_column(
Boolean,
default=True,
nullable=False,
)
minimum_match_score: Mapped[
float | None
] = mapped_column(
nullable=True,
)
send_immediately: Mapped[bool] = mapped_column(
Boolean,
default=True,
nullable=False,
)
include_in_digest: Mapped[bool] = mapped_column(
Boolean,
default=False,
nullable=False,
)

Collaboration Delivery Model

The earlier email system introduced delivery tracking.

We can generalize this pattern.

Create:

notification_deliveries

Fields:

id
notification_id
integration_id
channel
status
external_message_id
attempt_count
scheduled_at
delivered_at
last_attempt_at
error_message
deduplication_key

This table tracks whether a notification was delivered successfully to each external platform.


Delivery Channel Enum

class DeliveryChannel(str, Enum):
IN_APP = "in_app"
EMAIL = "email"
SLACK = "slack"
MICROSOFT_TEAMS = "microsoft_teams"

Delivery Status Enum

class DeliveryStatus(str, Enum):
PENDING = "pending"
PROCESSING = "processing"
DELIVERED = "delivered"
FAILED = "failed"
SKIPPED = "skipped"
CANCELLED = "cancelled"

Why Track Deliveries Separately?

A notification may have multiple deliveries.

Example:

Notification:
Tender deadline in two days
Deliveries:
In-app Delivered
Email Delivered
Slack Delivered
Microsoft Teams Failed

The notification represents the business event.

The delivery records represent communication attempts.

This model makes failures easier to diagnose.


Delivery Deduplication

Use a unique key such as:

notification_381:integration_24:slack

The same notification should not be sent repeatedly to the same integration unless an explicit retry is required.

Retries should update the existing delivery record rather than create a new one.


Provider Adapter Interface

Create:

app/integrations/collaboration/base.py
from abc import ABC, abstractmethod
class CollaborationAdapter(ABC):
@abstractmethod
def send_message(
self,
*,
webhook_url: str,
message,
):
raise NotImplementedError

Each provider implements the same interface.


Slack Adapter

Create:

app/integrations/collaboration/slack.py
class SlackAdapter(
CollaborationAdapter
):
def __init__(
self,
http_client,
):
self.http_client = http_client
def send_message(
self,
*,
webhook_url: str,
message: dict,
):
response = self.http_client.post(
webhook_url,
json=message,
timeout=10,
)
response.raise_for_status()
return {
"status": "delivered",
}

The HTTP client should support:

  • Connection timeouts
  • Read timeouts
  • Retry classification
  • Structured error handling

Microsoft Teams Adapter

Create:

app/integrations/collaboration/teams.py
class MicrosoftTeamsAdapter(
CollaborationAdapter
):
def __init__(
self,
http_client,
):
self.http_client = http_client
def send_message(
self,
*,
webhook_url: str,
message: dict,
):
response = self.http_client.post(
webhook_url,
json=message,
timeout=10,
)
response.raise_for_status()
return {
"status": "delivered",
}

Although the transport appears similar, message payloads should remain provider-specific.


Adapter Factory

Create:

app/integrations/collaboration/factory.py
class CollaborationAdapterFactory:
def __init__(
self,
slack_adapter,
teams_adapter,
):
self.slack_adapter = slack_adapter
self.teams_adapter = teams_adapter
def get_adapter(
self,
provider: CollaborationProvider,
) -> CollaborationAdapter:
if provider == CollaborationProvider.SLACK:
return self.slack_adapter
if (
provider ==
CollaborationProvider.MICROSOFT_TEAMS
):
return self.teams_adapter
raise ValueError(
f"Unsupported provider: {provider}"
)

The delivery service does not need provider-specific conditional logic beyond selecting the adapter.


Normalized Collaboration Message

Create a provider-independent message model.

app/schemas/collaboration_message.py
class CollaborationAction(BaseModel):
label: str
url: str
class CollaborationMessage(BaseModel):
title: str
summary: str
severity: NotificationSeverity
facts: dict[str, str]
actions: list[CollaborationAction]

Example normalized message:

CollaborationMessage(
title="New high-match tender",
summary=(
"Azure Cloud Migration Services "
"received a 94% match score."
),
severity=NotificationSeverity.INFO,
facts={
"Buyer": "City Digital Services",
"Deadline": "18 days",
"Contract Value": "€1,200,000",
"Pipeline Stage": "Not saved",
},
actions=[
CollaborationAction(
label="View Tender",
url=(
"https://app.example.com/"
"tenders/123"
),
)
],
)

Why Normalize Messages?

Without normalization, business logic may construct Slack and Teams payloads independently.

That creates duplication.

Better workflow:

Notification
|
v
Normalized Collaboration Message
|
+---- Slack Formatter
|
+---- Teams Formatter

Each formatter translates the same information into the provider’s preferred structure.


Slack Message Formatter

Create:

app/integrations/collaboration/slack_formatter.py

The formatter converts a normalized message into Slack-compatible blocks.

Conceptual payload:

def format_slack_message(
message: CollaborationMessage,
) -> dict:
fields = []
for label, value in message.facts.items():
fields.append(
{
"type": "mrkdwn",
"text": f"*{label}*\n{value}",
}
)
return {
"text": message.title,
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": message.title,
},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": message.summary,
},
},
{
"type": "section",
"fields": fields,
},
],
}

Add actions when present:

{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": action.label,
},
"url": action.url,
}
for action in message.actions
],
}

Microsoft Teams Message Formatter

Create:

app/integrations/collaboration/teams_formatter.py

The Teams formatter should produce a card-based payload.

Conceptual structure:

def format_teams_message(
message: CollaborationMessage,
) -> dict:
facts = [
{
"title": label,
"value": value,
}
for label, value
in message.facts.items()
]
return {
"type": "message",
"attachments": [
{
"contentType": (
"application/vnd.microsoft.card."
"adaptive"
),
"content": {
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"text": message.title,
"weight": "Bolder",
"size": "Large",
},
{
"type": "TextBlock",
"text": message.summary,
"wrap": True,
},
{
"type": "FactSet",
"facts": facts,
},
],
},
}
],
}

Actions can be added as card actions.


Avoid Provider Logic in Business Services

The opportunity-monitoring service should never build Slack blocks or Teams cards.

It should produce a domain-level notification.

Example:

notification = (
notification_service
.create_notification(...)
)

The delivery orchestrator handles channel formatting afterward.

This keeps tender logic independent from communication platforms.


Collaboration Delivery Service

Create:

app/services/collaboration_delivery_service.py

Responsibilities:

  • Load integration configuration
  • Check integration status
  • Check notification rules
  • Prevent duplicate delivery
  • Build normalized message
  • Select provider adapter
  • Format provider payload
  • Send message
  • Record delivery result
  • Schedule retries

Delivery Workflow

def deliver_notification(
self,
notification,
integration,
):
if not integration.is_active:
return self.skip_delivery(
reason="Integration disabled"
)
rule = self.rule_repository.get_rule(
integration_id=integration.id,
notification_type=(
notification.notification_type
),
)
if not rule or not rule.enabled:
return self.skip_delivery(
reason="Notification type disabled"
)
delivery = (
self.delivery_repository
.get_or_create(
notification_id=notification.id,
integration_id=integration.id,
)
)
if delivery.status == DeliveryStatus.DELIVERED:
return delivery
message = self.message_builder.build(
notification
)
payload = self.formatter_factory.format(
provider=integration.provider,
message=message,
)
return self.send_delivery(
delivery=delivery,
integration=integration,
payload=payload,
)

Message Builder

Create:

app/services/collaboration_message_builder.py

The builder converts notification records into collaboration messages.

Example for a deadline warning:

def build_deadline_message(
self,
notification,
opportunity,
):
return CollaborationMessage(
title="Tender deadline approaching",
summary=notification.message,
severity=notification.severity,
facts={
"Tender": opportunity.tender.title,
"Deadline": (
opportunity.tender.deadline
.isoformat()
),
"Pipeline Stage": (
opportunity.pipeline_stage.value
),
"Priority": opportunity.priority.value,
"Next Action": (
opportunity.next_action
or "Not defined"
),
},
actions=[
CollaborationAction(
label="Open Opportunity",
url=self.url_builder.opportunity_url(
opportunity.id
),
)
],
)

New High-Match Alert

A new high-match notification should include:

Tender title
Match score
Buyer
Country
Deadline
Estimated contract value
Top match reasons
Link to the Tender Detail Page

Example:

New High-Match Tender
Cloud Infrastructure Modernization
Match Score: 91%
Buyer: Regional Health Authority
Country: Netherlands
Deadline: 21 days
Value: €850,000
Why it matches:
• Azure architecture experience
• Healthcare delivery references
• Security and migration capabilities
View Tender

Keep collaboration messages concise.

The full analysis remains on the Tender Detail Page.


Deadline Alert

A deadline alert should emphasize urgency.

Include:

Tender title
Days remaining
Exact deadline
Pipeline stage
Owner
Next action
Link to workspace

Example:

Critical Deadline Alert
Digital Identity Platform
2 days remaining
Stage:
Preparing Bid
Owner:
Anna de Vries
Next Action:
Complete legal review

Overdue Action Alert

Example:

Internal Action Overdue
Tender:
Municipal Data Platform
Action:
Complete qualification review
Due:
Yesterday
Owner:
Bid Team
Open Opportunity

This helps collaboration tools function as operational work queues.


Daily Collaboration Digest

Not every event requires an immediate message.

A daily digest can summarize:

New high-match opportunities
Deadlines within seven days
Overdue internal actions
Recently updated tenders
Pipeline-stage changes

Example:

Daily Tender Opportunity Digest
New high-match tenders: 4
Deadlines within 7 days: 3
Overdue actions: 2
Updated tenders: 1
Top Opportunity:
Cloud Transformation Framework
Match Score: 96%
Value: €2.4M
Open Dashboard

One digest message reduces channel noise.


Preventing Notification Fatigue

A collaboration channel can quickly become unusable when every event generates a message.

Use several controls:

  • Minimum match-score thresholds
  • Severity thresholds
  • Daily digests
  • Notification-type filters
  • Channel-specific routing
  • Quiet hours
  • Deduplication
  • Rate limiting

Example routing:

#tender-opportunities
New high-match tenders
#bid-deadlines
Urgent deadlines and overdue actions
#procurement-management
Daily digest and pipeline summaries

Channel-Specific Routing

Organizations may create several integrations.

Example:

Integration 1:
Slack #tender-opportunities
Integration 2:
Slack #bid-deadlines
Integration 3:
Teams Procurement Management

Each integration has its own notification rules.

This is more flexible than one global channel.


Testing an Integration

Users should be able to test a connection before relying on it.

Create endpoint:

POST /collaboration-integrations/{id}/test

Example test message:

AI Tender Opportunity Scanner
Integration connected successfully.
Future tender alerts will appear in this channel.

A successful delivery updates:

last_successful_delivery_at
status = active

A failed delivery updates:

last_failed_delivery_at
status = error
last_error = sanitized error message

Integration Repository

Create:

app/repositories/collaboration_integration_repository.py

Responsibilities:

  • Create integration
  • List organization integrations
  • Load one integration
  • Update integration
  • Disable integration
  • Record successful delivery
  • Record failure
  • Delete integration

Example:

def list_active_for_organization(
self,
organization_id: int,
) -> list[CollaborationIntegration]:
statement = (
select(CollaborationIntegration)
.where(
CollaborationIntegration.organization_id
== organization_id,
CollaborationIntegration.is_active.is_(
True
),
)
)
return list(
self.db.scalars(statement).all()
)

Integration Service

Create:

app/services/collaboration_integration_service.py

Responsibilities:

  • Validate provider
  • Encrypt credentials
  • Create configuration
  • Enforce organization access
  • Test connections
  • Replace webhook credentials
  • Disable integrations
  • Delete integrations

Creating an Integration

def create_integration(
self,
*,
organization_id: int,
user_id: int,
data: CollaborationIntegrationCreate,
):
encrypted_url = (
self.encryption_service.encrypt(
str(data.webhook_url)
)
)
integration = CollaborationIntegration(
organization_id=organization_id,
created_by_user_id=user_id,
provider=data.provider,
display_name=data.display_name,
webhook_url_encrypted=encrypted_url,
workspace_name=data.workspace_name,
channel_name=data.channel_name,
)
created = self.repository.create(
integration
)
self.test_integration(
integration_id=created.id,
organization_id=organization_id,
)
return created

Consider keeping failed test integrations in an error state so users can correct the configuration.


Integration API

Create:

app/api/routes/collaboration_integrations.py

Recommended endpoints:

POST /collaboration-integrations
GET /collaboration-integrations
GET /collaboration-integrations/{id}
PATCH /collaboration-integrations/{id}
POST /collaboration-integrations/{id}/test
PUT /collaboration-integrations/{id}/credentials
DELETE /collaboration-integrations/{id}

Notification rule endpoints:

GET /collaboration-integrations/{id}/rules
PATCH /collaboration-integrations/{id}/rules

Create Integration Endpoint

@router.post(
"",
response_model=(
CollaborationIntegrationResponse
),
)
def create_collaboration_integration(
payload: CollaborationIntegrationCreate,
db: Session = Depends(get_db),
current_user: User = Depends(
get_current_user
),
):
service = (
build_collaboration_integration_service(
db
)
)
return service.create_integration(
organization_id=(
current_user.organization_id
),
user_id=current_user.id,
data=payload,
)

Authorization Requirements

Only authorized organization members should manage integrations.

Suggested roles:

Organization Owner
Administrator
Integration Manager

Regular members may be allowed to:

  • View connected channels
  • View delivery status
  • Use share actions

They should not automatically be able to:

  • Read or replace webhook credentials
  • Delete integrations
  • Change organization-wide notification rules

Frontend Structure

Create:

src/
├── pages/
├── IntegrationsPage.jsx
└── CollaborationIntegrationPage.jsx
├── components/
└── Integrations/
├── IntegrationCard.jsx
├── IntegrationForm.jsx
├── ProviderSelector.jsx
├── NotificationRuleForm.jsx
├── IntegrationStatusBadge.jsx
├── TestIntegrationButton.jsx
└── DeleteIntegrationDialog.jsx
├── hooks/
├── useCollaborationIntegrations.js
└── useIntegrationRules.js
└── services/
└── collaborationIntegrationService.js

Integrations Page

Example layout:

---------------------------------------------------------
Collaboration Integrations
---------------------------------------------------------
Send tender alerts to the channels where your team works.
Slack
Tender Opportunities
Workspace: Northstar Consulting
Channel: #tender-opportunities
Status: Connected
[Test] [Configure] [Disable]
---------------------------------------------------------
Microsoft Teams
Bid Management
Team: Procurement
Channel: Active Bids
Status: Connection Error
[Test] [Configure] [Remove]
---------------------------------------------------------
[Add Integration]

Provider Selector

Create:

ProviderSelector.jsx

Options:

Slack
Microsoft Teams

The form should display provider-specific setup instructions after selection.

Do not require users to understand the internal adapter architecture.


Integration Form

Fields:

Provider
Display name
Webhook URL
Workspace or team name
Channel name

Example:

<form onSubmit={handleSubmit}>
<ProviderSelector
value={form.provider}
onChange={handleProviderChange}
/>
<input
name="display_name"
placeholder="Tender Opportunities"
/>
<input
name="webhook_url"
type="password"
placeholder="Paste webhook URL"
/>
<input
name="channel_name"
placeholder="#tender-opportunities"
/>
<button type="submit">
Connect Integration
</button>
</form>

Use a password-style field to reduce accidental exposure.


Integration Status Badge

Create:

IntegrationStatusBadge.jsx

Possible states:

Connected
Disabled
Connection Error
Credentials Revoked

Do not display a successful status merely because a record exists.

The integration should be tested before it is marked connected.


Notification Rule Form

Example:

Notification Rules
[x] New high-match tenders
Minimum score: [85]
[x] Deadline warnings
[x] Urgent deadlines
[x] Overdue actions
[ ] Tender updates
[x] Daily digest
Delivery time: [08:30]

Rules should be stored per integration.


Delivering Existing Notifications

When an integration is created, should old notifications be sent?

Recommended default:

No

Only new events should be delivered.

Otherwise, connecting a new channel may flood it with historical alerts.

A manual option can be added:

Send current pipeline summary

This generates one summary instead of replaying all notifications.


Share Tender Action

Users may want to share a tender manually even when no automated rule applies.

Add a button to the Tender Detail Page:

Share to Slack or Teams

Workflow:

Select integration
|
v
Preview message
|
v
Send

Endpoint:

POST /tenders/{id}/share

Request:

{
"integration_id": 24,
"message": "Please review the technical requirements."
}

Manual Share Message

A manual share should include:

  • Tender title
  • AI summary
  • Match score
  • Deadline
  • Optional user note
  • Detail-page link

Example:

Tender Shared by Ben
Cloud Security Operations Framework
Match Score: 89%
Deadline: 14 days
Note:
Please review the certification requirements before tomorrow's bid meeting.
View Tender

Share Permissions

Users should only share through integrations belonging to their organization.

The backend must verify:

integration.organization_id
==
current_user.organization_id

Never trust an integration ID supplied by the frontend.


Background Delivery Task

External delivery should happen through background workers.

Create:

app/tasks/collaboration_tasks.py
def deliver_collaboration_notification(
delivery_id: int,
):
db = SessionLocal()
try:
service = (
build_collaboration_delivery_service(
db
)
)
service.process_delivery(
delivery_id
)
finally:
db.close()

The API or monitoring process creates the delivery record and queues the task.


Delivery Retry Strategy

Temporary errors may include:

  • Network timeout
  • Rate limiting
  • Temporary provider outage
  • Server-side provider error

Retry schedule:

Attempt 1: immediately
Attempt 2: after 1 minute
Attempt 3: after 5 minutes
Attempt 4: after 30 minutes
Attempt 5: after 2 hours

Permanent errors may include:

  • Invalid webhook
  • Revoked credentials
  • Unsupported payload
  • Deleted channel

Permanent failures should mark the integration as requiring attention.


Error Sanitization

Never store raw errors containing secret URLs.

Bad:

POST https://secret-webhook-url failed

Better:

Slack delivery failed with HTTP 403.
The integration credentials may be invalid or revoked.

Logs should include:

integration_id
provider
delivery_id
HTTP status
attempt count

They should not include the decrypted credential.


Rate Limiting

Collaboration providers may limit message volume.

The delivery worker should support:

  • Rate-limit detection
  • Retry-after headers
  • Exponential backoff
  • Per-integration throttling
  • Batch digests

A flood of 100 newly ingested tenders should not generate 100 immediate channel messages.

Possible batching rule:

More than 10 matching tenders within 15 minutes
|
v
Send one summary message

Message Size Limits

Tender descriptions can be very long.

Never send complete tender documents into Slack or Teams.

Messages should contain:

  • Short title
  • One concise summary
  • Essential facts
  • One or two actions
  • Link to full details

The application remains the source of truth.

Collaboration platforms provide awareness and discussion.


Interactive Actions

Incoming webhooks generally support limited interaction.

An installed application can support actions such as:

Save Tender
Assign to Me
Move to Reviewing
Dismiss
Open Opportunity

Example interaction flow:

User clicks "Save Tender"
|
v
Slack or Teams sends action payload
|
v
Backend verifies request signature
|
v
Backend identifies user
|
v
Saved opportunity is created
|
v
Message is updated

This requires a deeper application integration than outbound webhooks.


OAuth-Based Integration Roadmap

A future installed application will require:

Authorization request
OAuth callback
Access-token storage
Token refresh
Workspace installation record
Channel selection
Permission scopes
Token revocation

Possible database fields:

access_token_encrypted
refresh_token_encrypted
token_expires_at
external_workspace_id
external_installer_user_id

Do not add these fields prematurely if the MVP only uses webhooks.


User Identity Mapping

Two-way actions require mapping platform users to application users.

Example table:

collaboration_user_mappings

Fields:

id
integration_id
application_user_id
external_user_id
external_display_name
verified_at

Without identity mapping, the backend cannot safely determine which application user clicked an external action.


Conversation Threads

A future integration could create one discussion thread per tender.

Example:

Channel:
#tender-opportunities
Message:
Cloud Infrastructure Framework
Thread:
Technical requirements reviewed
Partner discussion
Pricing estimate
Bid decision

The application could store:

external_message_id
external_thread_id

This would allow future updates to be posted into the same conversation.


Tender Update Messages

When a saved tender changes, post a concise comparison.

Example:

Tender Updated
Digital Workplace Services
Changes detected:
• Deadline moved from 12 August to 19 August
• Contract value increased from €500K to €650K
• Security requirements updated
Review Changes

Do not send a generic “tender updated” message when the changed fields are known.

Specific changes are more actionable.


Pipeline Stage Notifications

Teams may want updates when important pipeline transitions occur.

Examples:

Opportunity moved to Preparing Bid
Bid submitted
Opportunity marked Won
Opportunity marked Lost

These should be configurable.

A busy team may only want:

Submitted
Won
Lost

while a small bid team may want every stage change.


Activity Audit Trail

Record manual shares and automated deliveries.

Example activity events:

Tender shared to Slack
Deadline alert delivered to Teams
Integration test failed
Daily digest delivered
Webhook credentials replaced
Integration disabled

This supports troubleshooting and enterprise audit requirements.


Monitoring Integration Health

Track:

Last successful delivery
Last failed delivery
Recent failure count
Average delivery time
Pending delivery count

An integration-health card might show:

Slack — Tender Opportunities
Status: Connected
Last successful delivery: 6 minutes ago
Failed deliveries in 24 hours: 0

Problem state:

Microsoft Teams — Active Bids
Status: Connection Error
Last successful delivery: 3 days ago
Recent error: Credentials rejected

Integration Health Endpoint

Create:

GET /collaboration-integrations/{id}/health

Response:

{
"status": "active",
"last_successful_delivery_at": "2026-08-14T08:15:00Z",
"last_failed_delivery_at": null,
"failed_deliveries_24h": 0,
"pending_deliveries": 2
}

Logging and Observability

Structured delivery log example:

collaboration_delivery_started
provider=slack
integration_id=24
delivery_id=918
collaboration_delivery_succeeded
provider=slack
integration_id=24
delivery_id=918
duration_ms=342

Failure:

collaboration_delivery_failed
provider=microsoft_teams
integration_id=31
delivery_id=922
http_status=429
retry_scheduled=true
attempt=2

Important metrics:

Messages queued
Messages delivered
Messages failed
Retry count
Delivery latency
Rate-limit events
Disabled integrations
Test-message failures

Security Requirements

Slack and Teams integrations introduce sensitive credentials and commercial information.

Security controls should include:

  • Encryption at rest for credentials
  • HTTPS-only provider endpoints
  • Strict organization-level authorization
  • Secret masking
  • Credential replacement
  • Delivery audit logs
  • Request-signature validation for interactive actions
  • Least-privilege scopes for installed applications
  • Token revocation support
  • Retention rules for delivery payloads

Do not store full tender documents in delivery logs.


URL Validation

Do not accept arbitrary outbound URLs without validation.

A malicious user could attempt to use the integration system to send requests to internal infrastructure.

The service should:

  • Require HTTPS
  • Validate supported provider domains where appropriate
  • Reject local and private-network destinations
  • Resolve and inspect redirect targets carefully
  • Apply outbound request timeouts
  • Limit response sizes

This reduces server-side request forgery risk.


Data Privacy

Before sending information to external platforms, consider what data should leave the application.

Avoid sending:

  • Confidential internal notes
  • Sensitive pricing strategies
  • Personal data
  • Restricted customer information
  • Private attachments
  • Full tender documents

Allow organizations to configure whether internal next actions and ownership details may appear in external messages.


Retention Policy

Delivery records may contain:

  • Message titles
  • Tender identifiers
  • Error information
  • External message IDs

Define how long this data should be retained.

Example:

Successful delivery metadata:
90 days
Failed delivery records:
180 days
Raw provider response data:
Do not retain unless required

Backend Testing

Create:

tests/services/
├── test_collaboration_integration_service.py
├── test_collaboration_delivery_service.py
└── test_collaboration_message_builder.py

Test:

  • Integration creation
  • Credential encryption
  • URL validation
  • Connection testing
  • Organization authorization
  • Rule evaluation
  • Message normalization
  • Slack formatting
  • Teams formatting
  • Delivery success
  • Duplicate prevention
  • Retry scheduling
  • Permanent failure handling
  • Integration health updates

Adapter Tests

Mock external HTTP calls.

Example:

def test_slack_adapter_sends_payload(
mock_http_client,
slack_adapter,
):
mock_http_client.post.return_value.raise_for_status.return_value = None
payload = {
"text": "Test message"
}
slack_adapter.send_message(
webhook_url=(
"https://example.invalid/webhook"
),
message=payload,
)
mock_http_client.post.assert_called_once()

Never send real external messages during automated tests.


Formatter Tests

Verify that a normalized message produces:

  • Title
  • Summary
  • Facts
  • Action buttons
  • Valid provider payload
  • No empty fields
  • No excessively long values

Use snapshot tests carefully.

Snapshots can help detect unexpected payload changes, but they should not replace semantic assertions.


Authorization Test

def test_user_cannot_manage_other_organization_integration(
service,
):
with pytest.raises(PermissionError):
service.get_integration(
integration_id=44,
organization_id=999,
)

Secret Exposure Test

Verify that:

GET /collaboration-integrations

does not return:

webhook_url
webhook_url_encrypted
access_token
refresh_token

Sensitive fields must be excluded from response schemas.


Delivery Deduplication Test

def test_notification_is_sent_once_per_integration(
delivery_service,
delivery_repository,
):
first = delivery_service.deliver(
notification_id=100,
integration_id=20,
)
second = delivery_service.deliver(
notification_id=100,
integration_id=20,
)
assert first.id == second.id
assert delivery_repository.count() == 1

Frontend Testing

Test:

  • Integrations list renders
  • Add-integration form validates
  • Provider selection works
  • Webhook secret remains masked
  • Test message works
  • Status badges render
  • Notification rules save
  • Integrations can be disabled
  • Delete confirmation works
  • Connection errors display safely
  • Manual tender sharing works

End-to-End Test Scenario

A useful complete integration test is:

1. Create a Slack integration
2. Enter a test webhook credential
3. Send a connection test
4. Confirm integration becomes active
5. Enable high-match notifications above 85%
6. Ingest a new tender
7. Generate a 92% match
8. Create a notification
9. Create a Slack delivery record
10. Process the delivery task
11. Confirm the external message was accepted
12. Confirm delivery status becomes delivered
13. Open the message link
14. Verify the Tender Detail Page opens

Repeat the workflow using the Teams adapter.


Current User Workflow

The platform now supports:

Tender Sources
|
v
AI Ingestion and Analysis
|
v
Personalized Matching
|
v
Recommendation Dashboard
|
v
Tender Detail Page
|
v
Saved Opportunities Workspace
|
v
Pipeline Analytics
|
v
Notifications
|
+---- In-App
|
+---- Email
|
+---- Slack
|
+---- Microsoft Teams

Tender intelligence is now delivered directly into the organization’s daily workflow.


Current Deliverables

At this stage, we have designed:

✔ Collaboration integration model
✔ Slack provider support
✔ Microsoft Teams provider support
✔ Integration-status tracking
✔ Webhook credential encryption
✔ Notification routing rules
✔ Shared delivery records
✔ Delivery deduplication
✔ Provider adapter interface
✔ Slack message formatter
✔ Teams message formatter
✔ Normalized message model
✔ Collaboration delivery service
✔ Connection testing
✔ Retry handling
✔ Rate-limit handling
✔ Daily channel digests
✔ Manual tender sharing
✔ Integration health monitoring
✔ Organization-level authorization
✔ Secret masking
✔ URL validation
✔ Security controls
✔ Backend tests
✔ Frontend integration settings
✔ End-to-end testing strategy

The AI Tender Opportunity Scanner can now deliver actionable tender intelligence directly into Slack and Microsoft Teams.


Recommended Git Commit

git add .
git commit -m "Build Slack and Teams tender integrations"

Push the changes:

git push origin main

What We Will Build Next

The platform now supports shared tender collaboration, but organization-level access is still only partially defined.

The next development stage will introduce:

  • User registration
  • Secure login
  • Organization accounts
  • Team membership
  • Role-based access control
  • Invitation workflows
  • Protected API routes
  • Tenant-level data isolation

The next article in the development series is:

“Building Authentication, Organization Accounts, and Role-Based Access Control.”


Conclusion

Slack and Microsoft Teams integrations move tender intelligence into the collaboration environments where bid teams already communicate and make decisions.

By introducing organization-level integration records, encrypted webhook credentials, provider adapters, normalized message models, channel-specific notification rules, delivery tracking, retry handling, and integration-health monitoring, the platform gains a reusable multi-channel collaboration architecture.

Teams can now receive high-match tender alerts, urgent deadline reminders, overdue action warnings, tender updates, and daily pipeline digests without continuously monitoring the application.

The integrations begin with secure outbound notifications, while the architecture leaves room for richer features such as interactive actions, user identity mapping, threaded tender discussions, OAuth-based app installations, and two-way bid collaboration.

With external collaboration in place, the next step is securing the platform with authentication, organization accounts, team membership, and role-based access control.

Discover more from BidRadar

Subscribe now to keep reading and get access to the full archive.

Continue reading