Overview
- Purpose: Microservice for integrating with X (Twitter) platform — managing Twitter accounts, conversations, contacts, campaigns, templates, automation flows, AI conversation sessions, and audience segments.
- Port: 5000 (configured in Program.cs via ASPNETCORE_URLS)
- Database: PostgreSQL (connection string:
DefaultConnection or DATABASE_URL)
- Architecture: Clean Architecture + CQRS (MediatR 12.4.1)
- API Versioning: URL segment
api/v{version:apiVersion} (v1.0)
API Endpoints
AccountsController (api/v1/accounts)
| Method |
Route |
Description |
| GET |
/accounts?merchantId |
Get Twitter accounts for a merchant |
| GET |
/accounts/{id} |
Get Twitter account by ID |
| POST |
/accounts/connect |
Connect a new Twitter account |
| POST |
/accounts/{id}/disconnect |
Disconnect a Twitter account |
| POST |
/accounts/{id}/refresh-tokens |
Refresh OAuth tokens |
CampaignsController (api/v1/campaigns)
| Method |
Route |
Description |
| GET |
/campaigns?merchantId&status&skip&take |
List campaigns with filtering + pagination |
| GET |
/campaigns/{id} |
Get campaign details |
| POST |
/campaigns |
Create a new campaign |
| POST |
/campaigns/{id}/start |
Start a campaign |
| POST |
/campaigns/{id}/pause |
Pause a running campaign |
| POST |
/campaigns/{id}/resume |
Resume a paused campaign |
| POST |
/campaigns/{id}/cancel |
Cancel a campaign |
ContactsController (api/v1/contacts)
| Method |
Route |
Description |
| GET |
/contacts?accountId&search&tags&skip&take |
List contacts with search + filtering |
| GET |
/contacts/{id} |
Get contact details |
ConversationsController (api/v1/conversations)
| Method |
Route |
Description |
| GET |
/conversations?accountId&status&assignedToUserId&skip&take |
List conversations with filtering |
| GET |
/conversations/{id}?includeMessages |
Get conversation details (optionally with messages) |
| POST |
/conversations/{id}/messages |
Send a message in a conversation |
| POST |
/conversations/{id}/close |
Close a conversation |
| POST |
/conversations/{id}/reopen |
Reopen a closed conversation |
| POST |
/conversations/{id}/assign |
Assign conversation to a user |
| POST |
/conversations/{id}/pending |
Mark conversation as pending |
TemplatesController (api/v1/templates)
| Method |
Route |
Description |
| GET |
/templates?merchantId&type |
List templates by merchant and type |
| GET |
/templates/{id} |
Get template details |
SamplesController (api/v1/samples)
| Method |
Route |
Description |
| GET |
/samples |
List all samples |
| GET |
/samples/{id} |
Get sample by ID |
| POST |
/samples |
Create a sample |
| PUT |
/samples/{id} |
Update a sample |
| DELETE |
/samples/{id} |
Delete a sample |
| PATCH |
/samples/{id}/status |
Change sample status |
WebhooksController (api/v1/webhooks)
| Method |
Route |
Description |
| GET |
/webhooks/twitter |
Twitter CRC challenge verification |
| POST |
/webhooks/twitter |
Receive Twitter webhook events (DMs, follows, etc.) |
| POST |
/webhooks/twitter/register |
Register webhook URL with Twitter |
| POST |
/webhooks/twitter/subscribe |
Subscribe to Twitter webhook events |
Commands (28 total)
Twitter Account Commands (3)
| Command |
Result |
Description |
ConnectTwitterAccountCommand(MerchantId, TwitterUserId, Username, OAuthToken, OAuthTokenSecret, DisplayName?, ProfileImageUrl?) |
ConnectTwitterAccountResult(Success, AccountId?, Error?) |
Connect a Twitter account |
DisconnectTwitterAccountCommand(AccountId) |
bool |
Disconnect a Twitter account |
RefreshTwitterTokensCommand(AccountId, NewOAuthToken, NewOAuthTokenSecret) |
bool |
Refresh OAuth tokens |
Campaign Commands (6)
| Command |
Result |
Description |
CreateCampaignCommand(MerchantId, Name, Type) |
CreateCampaignResult(Success, CampaignId?, Error?) |
Create a campaign (types: bulk_dm, welcome, follow_up, drip, broadcast) |
StartCampaignCommand(CampaignId) |
bool |
Start a campaign |
PauseCampaignCommand(CampaignId) |
bool |
Pause a running campaign |
ResumeCampaignCommand(CampaignId) |
bool |
Resume a paused campaign |
CancelCampaignCommand(CampaignId) |
bool |
Cancel a campaign |
UpdateCampaignMetricsCommand(CampaignId, Sent, Delivered, Read, Clicked, Failed) |
bool |
Update campaign metrics |
Conversation Commands (5)
| Command |
Result |
Description |
SendMessageCommand(ConversationId, Content, MediaUrls?) |
bool |
Send a message (max 4 media) |
CloseConversationCommand(ConversationId, Reason?) |
bool |
Close a conversation (reason max 500 chars) |
ReopenConversationCommand(ConversationId) |
bool |
Reopen a conversation |
AssignConversationCommand(ConversationId, UserId) |
bool |
Assign to a user |
MarkConversationPendingCommand(ConversationId) |
bool |
Mark as pending |
Contact Commands (5)
| Command |
Result |
Description |
CreateContactCommand(AccountId, TwitterUserId, Username, Source, DisplayName?, ProfileImageUrl?, Attributes?, Tags?) |
CreateContactResult(Success, ContactId?, Error?) |
Create a contact |
UpdateContactCommand(ContactId, Username, DisplayName?, ProfileImageUrl?) |
bool |
Update contact profile |
AddContactTagCommand(ContactId, TagName) |
bool |
Add tag to contact |
RemoveContactTagCommand(ContactId, TagName) |
bool |
Remove tag from contact |
UpdateContactAttributesCommand(ContactId, Attributes) |
bool |
Update custom attributes |
Template Commands (4)
| Command |
Result |
Description |
CreateTemplateCommand(MerchantId, Name, Type, Content) |
CreateTemplateResult(Success, TemplateId?, Error?) |
Create a template |
UpdateTemplateCommand(TemplateId, Name, Content) |
bool |
Update a template |
DeleteTemplateCommand(TemplateId) |
bool |
Delete a template |
PreviewTemplateCommand(TemplateId, Variables) |
PreviewTemplateResult(Success, RenderedContent?, Error?) |
Preview template rendering |
Sample Commands (4)
| Command |
Result |
Description |
CreateSampleCommand(Name, Description?) |
CreateSampleResult(Success, SampleId?, Error?) |
Create a sample |
UpdateSampleCommand(Id, Name, Description?) |
bool |
Update a sample |
DeleteSampleCommand(Id) |
bool |
Delete a sample |
ChangeSampleStatusCommand(Id, NewStatus) |
bool |
Change status (Draft->Active->Completed, Draft/Active->Cancelled) |
Queries
| Query |
Result |
GetTwitterAccountsQuery(MerchantId) |
List<TwitterAccountDto> |
GetTwitterAccountByIdQuery(AccountId) |
TwitterAccountDto? |
Campaign Queries
| Query |
Result |
GetCampaignsQuery(MerchantId, Status?, Skip, Take) |
CampaignsResult(Items, TotalCount) |
GetCampaignByIdQuery(CampaignId) |
CampaignDetailDto? |
Conversation Queries
| Query |
Result |
GetConversationsQuery(AccountId, Status?, AssignedToUserId?, Skip, Take) |
ConversationsResult(Items, TotalCount) |
GetConversationByIdQuery(ConversationId, IncludeMessages) |
ConversationDetailDto? |
Contact Queries
| Query |
Result |
GetContactsQuery(AccountId, Search?, Tags?, Skip, Take) |
ContactsResult(Items, TotalCount) |
GetContactByIdQuery(ContactId) |
ContactDetailDto? |
Template Queries
| Query |
Result |
GetTemplatesQuery(MerchantId, Type?) |
List<TemplateDto> |
GetTemplateByIdQuery(TemplateId) |
TemplateDetailDto? |
Sample Queries
| Query |
Result |
GetSamplesQuery |
List<SampleDto> |
GetSampleByIdQuery(Id) |
SampleDto? |
DTOs
- TwitterAccountDto: Id, MerchantId, TwitterUserId, Username, DisplayName, ProfileImageUrl, Status, ConnectedAt
- CampaignDto: Id, MerchantId, Name, Type, Status, CreatedAt, UpdatedAt
- CampaignDetailDto: CampaignDto + TemplateId, SegmentIds, Schedule (StartAt, EndAt, TimeZone), Metrics (Sent, Delivered, Read, Clicked, Failed)
- ConversationDto: Id, AccountId, ContactId, Status, LastMessagePreview, LastMessageAt, AssignedToUserId
- ConversationDetailDto: ConversationDto + Messages (list of MessageDto)
- MessageDto: Id, Content, Direction, Type, SentAt, IsFromBot, TwitterMessageId
- ContactDto: Id, TwitterUserId, Username, DisplayName, ProfileImageUrl, Tags, LastInteraction
- ContactDetailDto: ContactDto + Attributes, Source, CreatedAt
- TemplateDto: Id, Name, Type, CreatedAt
- TemplateDetailDto: TemplateDto + Content, Variables (extracted from {{var}} placeholders)
Domain Model
Aggregates
- Properties: MerchantId, TwitterUserId, Username, DisplayName, ProfileImageUrl, StatusId (TwitterAccountStatus), OAuthToken, OAuthTokenSecret, WebhookId, Settings (Dictionary), ConnectedAt, CreatedAt, UpdatedAt
- Methods: Activate(), SetWebhookId(), UpdateCredentials(), UpdateProfile(), UpdateSettings(), MarkAsError(reason), Deactivate(), Disconnect()
- Domain Events: TwitterAccountConnectedDomainEvent, TwitterAccountStatusChangedDomainEvent, TwitterAccountDisconnectedDomainEvent
- Pending(1), Active(2), Inactive(3), Error(4), Disconnected(5)
Campaign (Aggregate Root)
- Properties: MerchantId, Name, Type, StatusId (CampaignStatus), TemplateId, SegmentIds (List - JSONB), Schedule (CampaignSchedule value object), Metrics (CampaignMetrics value object), CreatedAt, UpdatedAt
- Value Objects:
- CampaignSchedule: StartAt, EndAt, TimeZone
- CampaignMetrics: Sent, Delivered, Read, Clicked, Failed
- Methods: SetTemplate(), AddSegment(), RemoveSegment(), SetSchedule(), Start(), Pause(), Resume(), Complete(), Cancel(), UpdateMetrics()
- Domain Events: CampaignStartedDomainEvent, CampaignCompletedDomainEvent
CampaignStatus (Enumeration)
- Draft(1), Scheduled(2), Running(3), Paused(4), Completed(5), Cancelled(6)
Conversation (Aggregate Root)
- Properties: AccountId, ContactId, Status (ConversationStatus), AssignedToUserId, StartedAt, EndedAt, LastMessagePreview, LastMessageAt, CreatedAt, UpdatedAt
- Child Entity: Message — Direction (inbound/outbound), Type (text/image/card/quick_reply), Content, Attachments (MessageAttachment value object with Url, Type), IsFromBot, TwitterMessageId, SentAt
- Methods: AddMessage(), Close(reason?), Reopen(), AssignTo(userId), Unassign(), MarkAsPending()
- Domain Events: ConversationStartedDomainEvent, MessageReceivedDomainEvent, MessageSentDomainEvent
ConversationStatus (Enumeration)
- Open(1), Closed(2), Pending(3)
Contact (Aggregate Root)
- Properties: AccountId, TwitterUserId, Username, DisplayName, ProfileImageUrl, Source, Attributes (Dictionary<string,object>), CreatedAt, UpdatedAt, LastInteractionAt
- Child Entity: ContactTag — Name (normalized lowercase), CreatedAt
- Methods: UpdateProfile(), AddTag(), RemoveTag(), SetAttribute(), RemoveAttribute(), UpdateAttributes(), RecordInteraction()
- Domain Events: ContactCreatedDomainEvent, ContactTaggedDomainEvent
Template (Aggregate Root)
- Properties: MerchantId, Name, Type, Content, Variables (extracted from
{{variable}} via regex), CreatedAt, UpdatedAt
- Constants: MaxContentLength = 10000
- Methods: Update(name, content), Render(variables), ValidateVariables(variables), GetMissingVariables(variables)
Sample (Aggregate Root — template/demo)
- Properties: Name, Description, StatusId (SampleStatus), CreatedAt, UpdatedAt
- Status Transitions: Draft->Active->Completed, Draft/Active->Cancelled
- Methods: Activate(), Complete(), Cancel()
- Domain Events: SampleCreatedDomainEvent, SampleStatusChangedDomainEvent
Segment (Aggregate Root)
- Properties: AccountId, Name, Description, CreatedAt, UpdatedAt
- Child Entity: SegmentCondition — Field, Operator, Value
AIConversationSession (Aggregate Root)
- Properties: ConversationId, Intent, Slots (Dictionary), ContextItems (List), IsActive, CreatedAt, UpdatedAt, ExpiresAt
- Constants: MaxContextItems = 10, EscalationThreshold = 0.6
- Methods: AddContext(), ClearContext(), SetIntent(), SetSlot(), Deactivate(), Reactivate(), IsExpired()
- Domain Events: AIEscalationRequestedDomainEvent, IntentDetectedDomainEvent
AutomationFlow (Aggregate Root)
- Properties: AccountId, Name, Description, StatusId (FlowStatus), CreatedAt, UpdatedAt
- Child Entities: FlowTrigger, FlowNode, FlowConnection
- FlowStatus: Draft(1), Active(2), Inactive(3)
- Methods: AddNode(), RemoveNode(), Connect(), Activate(), Deactivate()
Database Schema
DbContext: MktXServiceContext (17 DbSets)
- Implements IUnitOfWork, dispatches domain events before SaveChanges
- Transaction support: BeginTransactionAsync, CommitTransactionAsync, RollbackTransaction
Tables
| Column |
Type |
Constraints |
| id |
uuid |
PK |
| merchant_id |
uuid |
NOT NULL, indexed |
| twitter_user_id |
varchar |
NOT NULL, unique index |
| username |
varchar |
|
| display_name |
varchar |
|
| profile_image_url |
varchar |
|
| status_id |
int |
FK -> twitter_account_statuses, indexed |
| oauth_token |
varchar |
|
| oauth_token_secret |
varchar |
|
| webhook_id |
varchar |
|
| settings |
jsonb |
|
| connected_at |
timestamp |
|
| created_at |
timestamp |
NOT NULL |
| updated_at |
timestamp |
NOT NULL |
Indexes: ix_twitter_accounts_twitter_user_id (unique), ix_twitter_accounts_merchant_id, ix_twitter_accounts_status_id
| Column |
Type |
| id |
int |
| name |
varchar |
Seed data: 1=Pending, 2=Active, 3=Inactive, 4=Error, 5=Disconnected
campaigns
| Column |
Type |
Constraints |
| id |
uuid |
PK |
| merchant_id |
uuid |
NOT NULL, indexed |
| name |
varchar |
NOT NULL |
| type |
varchar |
NOT NULL |
| status_id |
int |
FK -> campaign_statuses, indexed |
| template_id |
uuid |
nullable |
| segment_ids |
jsonb |
List |
| schedule |
jsonb |
CampaignSchedule value object |
| metrics |
jsonb |
CampaignMetrics value object |
| created_at |
timestamp |
NOT NULL, desc index |
| updated_at |
timestamp |
NOT NULL |
Indexes: ix_campaigns_merchant_id, ix_campaigns_status_id, ix_campaigns_created_at (desc), ix_campaigns_merchant_status (composite)
campaign_statuses
| Column |
Type |
| id |
int |
| name |
varchar |
Seed data: 1=Draft, 2=Scheduled, 3=Running, 4=Paused, 5=Completed, 6=Cancelled
conversations, messages, contacts, contact_tags, templates, samples, segments, segment_conditions, ai_conversation_sessions, automation_flows, flow_triggers, flow_nodes, flow_connections
(Configured via EF Core FluentAPI — snake_case columns, private field mapping)
Domain Events (14 total)
| Event |
Payload |
| SampleCreatedDomainEvent |
Sample entity |
| SampleStatusChangedDomainEvent |
Sample entity |
| TwitterAccountConnectedDomainEvent |
TwitterAccount entity |
| TwitterAccountDisconnectedDomainEvent |
TwitterAccount entity |
| TwitterAccountStatusChangedDomainEvent |
TwitterAccount entity |
| CampaignStartedDomainEvent |
Campaign entity |
| CampaignCompletedDomainEvent |
Campaign entity |
| ConversationStartedDomainEvent |
Conversation entity |
| MessageReceivedDomainEvent |
Conversation entity |
| MessageSentDomainEvent |
Conversation entity |
| ContactCreatedDomainEvent |
Contact entity |
| ContactTaggedDomainEvent |
Contact entity, TagName |
| AIEscalationRequestedDomainEvent |
AIConversationSession entity |
| IntentDetectedDomainEvent |
AIConversationSession entity |
Validators
| Validator |
Rules |
| CreateCampaignCommandValidator |
MerchantId required; Name required, max 255; Type in [bulk_dm, welcome, follow_up, drip, broadcast] |
| StartCampaignCommandValidator |
CampaignId required |
| PauseCampaignCommandValidator |
CampaignId required |
| ResumeCampaignCommandValidator |
CampaignId required |
| CancelCampaignCommandValidator |
CampaignId required |
| UpdateCampaignMetricsCommandValidator |
CampaignId required; all metric fields >= 0 |
| ConnectTwitterAccountCommandValidator |
MerchantId required; TwitterUserId required; Username required; OAuthToken required; OAuthTokenSecret required |
| DisconnectTwitterAccountCommandValidator |
AccountId required |
| RefreshTwitterTokensCommandValidator |
AccountId required; NewOAuthToken required; NewOAuthTokenSecret required |
| SendMessageCommandValidator |
ConversationId required; Content required, max 10000; MediaUrls max 4 items |
| CloseConversationCommandValidator |
ConversationId required; Reason max 500 chars |
| AssignConversationCommandValidator |
ConversationId required; UserId required |
| ReopenConversationCommandValidator |
ConversationId required |
| MarkConversationPendingCommandValidator |
ConversationId required |
| CreateSampleCommandValidator |
Name required, max 200 |
| UpdateSampleCommandValidator |
Id required; Name required, max 200 |
External Services
- Authentication: OAuth 1.0a HMAC-SHA1 signing
- Resilience: Polly retry (3 attempts, exponential backoff) + circuit breaker (5 failures, 1 min break)
- Methods: SendDirectMessage, GetDirectMessages, GetUserById, GetUserByUsername, RegisterWebhook, SubscribeWebhook, DeleteWebhook, UploadMedia, GetRateLimitStatus
- Config: TwitterApiOptions (ConsumerKey, ConsumerSecret, AccessToken, AccessTokenSecret, EnvironmentName)
OpenAI Service Client (IAIServiceClient / OpenAIServiceClient)
- Resilience: Circuit breaker
- Methods: GetChatCompletionAsync, GetChatCompletionWithFunctionsAsync, StreamChatCompletionAsync, DetectIntentAsync, ExtractEntitiesAsync, GetEmbeddingsAsync, ModerateContentAsync
- Config: OpenAIOptions (ApiKey, DefaultModel=gpt-4o-mini, EmbeddingModel=text-embedding-3-small, MaxTokens, DefaultTemperature)
Dependencies (NuGet)
API Project
- MediatR 12.4.1
- FluentValidation.DependencyInjectionExtensions 11.11.0
- Swashbuckle.AspNetCore 7.2.0
- Asp.Versioning.Mvc 8.1.0
- Asp.Versioning.Mvc.ApiExplorer 8.1.0
- Hellang.Middleware.ProblemDetails 6.5.1
- Serilog.AspNetCore 8.0.3
- AspNetCore.HealthChecks.NpgSql 8.0.2
Infrastructure Project
- Microsoft.EntityFrameworkCore 10.0.0
- Npgsql.EntityFrameworkCore.PostgreSQL 10.0.0
- MediatR 12.4.1
- Dapper 2.1.35
- Polly 8.5.0
- Polly.Extensions.Http 3.0.0
- StackExchange.Redis 2.8.16
- Quartz 3.13.1
Configuration
Environment Variables / Settings
ConnectionStrings:DefaultConnection or DATABASE_URL — PostgreSQL connection
TwitterApi:ConsumerKey — Twitter API consumer key
TwitterApi:ConsumerSecret — Twitter API consumer secret
TwitterApi:AccessToken — Twitter API access token
TwitterApi:AccessTokenSecret — Twitter API access token secret
TwitterApi:EnvironmentName — Twitter webhook environment
OpenAI:ApiKey — OpenAI API key
OpenAI:DefaultModel — Default model (gpt-4o-mini)
OpenAI:EmbeddingModel — Embedding model (text-embedding-3-small)
MediatR Pipeline Behaviors
- LoggingBehavior — Request/response logging with Stopwatch
- ValidatorBehavior — FluentValidation in pipeline
- TransactionBehavior — Auto transaction for Commands (skips Queries)
Health Checks
- PostgreSQL health check via NpgSql
Known Issues
- Incomplete DI Registration: Only
ISampleRepository is registered in DependencyInjection.cs. The other 8 repository interfaces (ITwitterAccountRepository, ICampaignRepository, IConversationRepository, IContactRepository, ITemplateRepository, ISegmentRepository, IAIConversationSessionRepository, IAutomationFlowRepository) are NOT registered. This means most functionality beyond Samples will fail at runtime until DI is fixed.