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 ScannerNew High-Match TenderAzure Cloud Migration ServicesMatch Score: 94%Buyer: City Digital ServicesContract Value: €1,200,000Deadline: 18 daysWhy 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 ApproachingCybersecurity Assessment FrameworkSubmission Deadline:3 days remainingPipeline Stage:Preparing BidNext Action:Complete pricing reviewPriority:Urgent[Open Opportunity Workspace]
The integrations will begin as outbound notification channels.
Later, they can support two-way actions such as:
Save OpportunityAssign OwnerChange Pipeline StageAdd Internal NoteSchedule Bid ReviewDismiss 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 | vUser opens platform | vUser copies tender link | vUser opens Slack or Teams | vUser writes summary | vTeam begins discussion
With integration:
Tender matched | vAlert appears in team channel | vTeam reviews opportunity | vDiscussion 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 | vNotification Service | vDelivery 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 notificationEmailSlackMicrosoft Teams
Do not create separate business logic for every channel.
Bad architecture:
Deadline checker sends Slack messageDeadline checker sends Teams messageDeadline checker sends emailDeadline checker creates in-app alert
Better architecture:
Deadline checker | vCreate notification event | vDelivery 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:
idorganization_idcreated_by_user_idproviderdisplay_namewebhook_url_encryptedworkspace_namechannel_namechannel_idis_activecreated_atupdated_atlast_successful_delivery_atlast_failed_delivery_atlast_error
The provider field supports:
slackmicrosoft_teams
Later, it can support:
discordgoogle_chatgeneric_webhook
Provider Enum
from enum import Enumclass 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 datetimefrom sqlalchemy import ( Boolean, DateTime, Enum as SqlEnum, ForeignKey, Integer, String, Text,)from sqlalchemy.orm import Mapped, mapped_columnfrom 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 ConsultingSlack Channel:#tender-opportunitiesUsers:Bid ManagerTechnical ConsultantSales DirectorDelivery 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:
idintegration_idnotification_typeminimum_severityminimum_match_scoreenabledsend_immediatelyinclude_in_digest
Example rules:
New high-match tender:Send when score >= 85Deadline warning:Send when 7 days remainDeadline urgent:Always sendAction overdue:SendDaily 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:
idnotification_idintegration_idchannelstatusexternal_message_idattempt_countscheduled_atdelivered_atlast_attempt_aterror_messagededuplication_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 daysDeliveries:In-app DeliveredEmail DeliveredSlack DeliveredMicrosoft 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, abstractmethodclass 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 | vNormalized 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 titleMatch scoreBuyerCountryDeadlineEstimated contract valueTop match reasonsLink to the Tender Detail Page
Example:
New High-Match TenderCloud Infrastructure ModernizationMatch Score: 91%Buyer: Regional Health AuthorityCountry: NetherlandsDeadline: 21 daysValue: €850,000Why it matches:• Azure architecture experience• Healthcare delivery references• Security and migration capabilitiesView Tender
Keep collaboration messages concise.
The full analysis remains on the Tender Detail Page.
Deadline Alert
A deadline alert should emphasize urgency.
Include:
Tender titleDays remainingExact deadlinePipeline stageOwnerNext actionLink to workspace
Example:
Critical Deadline AlertDigital Identity Platform2 days remainingStage:Preparing BidOwner:Anna de VriesNext Action:Complete legal review
Overdue Action Alert
Example:
Internal Action OverdueTender:Municipal Data PlatformAction:Complete qualification reviewDue:YesterdayOwner:Bid TeamOpen 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 opportunitiesDeadlines within seven daysOverdue internal actionsRecently updated tendersPipeline-stage changes
Example:
Daily Tender Opportunity DigestNew high-match tenders: 4Deadlines within 7 days: 3Overdue actions: 2Updated tenders: 1Top Opportunity:Cloud Transformation FrameworkMatch Score: 96%Value: €2.4MOpen 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-opportunitiesNew high-match tenders#bid-deadlinesUrgent deadlines and overdue actions#procurement-managementDaily digest and pipeline summaries
Channel-Specific Routing
Organizations may create several integrations.
Example:
Integration 1:Slack #tender-opportunitiesIntegration 2:Slack #bid-deadlinesIntegration 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 ScannerIntegration connected successfully.Future tender alerts will appear in this channel.
A successful delivery updates:
last_successful_delivery_atstatus = active
A failed delivery updates:
last_failed_delivery_atstatus = errorlast_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-integrationsGET /collaboration-integrationsGET /collaboration-integrations/{id}PATCH /collaboration-integrations/{id}POST /collaboration-integrations/{id}/testPUT /collaboration-integrations/{id}/credentialsDELETE /collaboration-integrations/{id}
Notification rule endpoints:
GET /collaboration-integrations/{id}/rulesPATCH /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 OwnerAdministratorIntegration 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.SlackTender OpportunitiesWorkspace: Northstar ConsultingChannel: #tender-opportunitiesStatus: Connected[Test] [Configure] [Disable]---------------------------------------------------------Microsoft TeamsBid ManagementTeam: ProcurementChannel: Active BidsStatus: Connection Error[Test] [Configure] [Remove]---------------------------------------------------------[Add Integration]
Provider Selector
Create:
ProviderSelector.jsx
Options:
SlackMicrosoft Teams
The form should display provider-specific setup instructions after selection.
Do not require users to understand the internal adapter architecture.
Integration Form
Fields:
ProviderDisplay nameWebhook URLWorkspace or team nameChannel 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:
ConnectedDisabledConnection ErrorCredentials 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 tendersMinimum score: [85][x] Deadline warnings[x] Urgent deadlines[x] Overdue actions[ ] Tender updates[x] Daily digestDelivery 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 | vPreview message | vSend
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 BenCloud Security Operations FrameworkMatch Score: 89%Deadline: 14 daysNote: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: immediatelyAttempt 2: after 1 minuteAttempt 3: after 5 minutesAttempt 4: after 30 minutesAttempt 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_idproviderdelivery_idHTTP statusattempt 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 | vSend 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 TenderAssign to MeMove to ReviewingDismissOpen Opportunity
Example interaction flow:
User clicks "Save Tender" | vSlack or Teams sends action payload | vBackend verifies request signature | vBackend identifies user | vSaved opportunity is created | vMessage is updated
This requires a deeper application integration than outbound webhooks.
OAuth-Based Integration Roadmap
A future installed application will require:
Authorization requestOAuth callbackAccess-token storageToken refreshWorkspace installation recordChannel selectionPermission scopesToken revocation
Possible database fields:
access_token_encryptedrefresh_token_encryptedtoken_expires_atexternal_workspace_idexternal_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:
idintegration_idapplication_user_idexternal_user_idexternal_display_nameverified_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-opportunitiesMessage:Cloud Infrastructure FrameworkThread:Technical requirements reviewedPartner discussionPricing estimateBid decision
The application could store:
external_message_idexternal_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 UpdatedDigital Workplace ServicesChanges detected:• Deadline moved from 12 August to 19 August• Contract value increased from €500K to €650K• Security requirements updatedReview 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 BidBid submittedOpportunity marked WonOpportunity marked Lost
These should be configurable.
A busy team may only want:
SubmittedWonLost
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 SlackDeadline alert delivered to TeamsIntegration test failedDaily digest deliveredWebhook credentials replacedIntegration disabled
This supports troubleshooting and enterprise audit requirements.
Monitoring Integration Health
Track:
Last successful deliveryLast failed deliveryRecent failure countAverage delivery timePending delivery count
An integration-health card might show:
Slack — Tender OpportunitiesStatus: ConnectedLast successful delivery: 6 minutes agoFailed deliveries in 24 hours: 0
Problem state:
Microsoft Teams — Active BidsStatus: Connection ErrorLast successful delivery: 3 days agoRecent 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_startedprovider=slackintegration_id=24delivery_id=918collaboration_delivery_succeededprovider=slackintegration_id=24delivery_id=918duration_ms=342
Failure:
collaboration_delivery_failedprovider=microsoft_teamsintegration_id=31delivery_id=922http_status=429retry_scheduled=trueattempt=2
Important metrics:
Messages queuedMessages deliveredMessages failedRetry countDelivery latencyRate-limit eventsDisabled integrationsTest-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 daysFailed delivery records:180 daysRaw 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_urlwebhook_url_encryptedaccess_tokenrefresh_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 integration2. Enter a test webhook credential3. Send a connection test4. Confirm integration becomes active5. Enable high-match notifications above 85%6. Ingest a new tender7. Generate a 92% match8. Create a notification9. Create a Slack delivery record10. Process the delivery task11. Confirm the external message was accepted12. Confirm delivery status becomes delivered13. Open the message link14. Verify the Tender Detail Page opens
Repeat the workflow using the Teams adapter.
Current User Workflow
The platform now supports:
Tender Sources | vAI Ingestion and Analysis | vPersonalized Matching | vRecommendation Dashboard | vTender Detail Page | vSaved Opportunities Workspace | vPipeline Analytics | vNotifications | +---- 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.