Request Flow Traces Chapter 08

Complete request lifecycle traces showing every layer a request passes through, step by step, with the actual code that executes. File paths reference the real codebase.

8.1 How to Read These Traces

Each trace below shows the complete path of a request through the system. Every numbered step represents actual code execution, with file paths you can open in your editor.

Conventions used in these traces: Step numbers flow top-to-bottom in execution order. File paths reference the real codebase under apps/ and pkg/. Code snippets are simplified for clarity but match the actual implementation. Cross-service calls (HTTP or Kafka) are explicitly marked as boundaries.

The gateway service sits in front of all requests. Depending on route configuration in apps/gateway-service/config/routes.yaml, the gateway either validates JWTs and injects tenant headers, or passes the request through directly. Individual services apply their own middleware chains on top.

8.2 TRACE 1: POST /api/v1/auth/login

The login flow is the most critical path in the system. It touches the gateway, auth-service, admin-service (cross-service HTTP), PostgreSQL, Redis, and JWT generation.

Step 1: Client sends request

The client sends a POST to the gateway with email and password.

POST https://acme.app.com:8080/api/v1/auth/login
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "secret123"
}

Step 2: Gateway middleware chain

apps/gateway-service/cmd/server/main.go:130-134

The request enters the gateway and passes through the middleware chain. Middleware is applied in reverse order (last .Use() is outermost).

// Applied bottom-to-top: tracing wraps everything, tenant is next, etc.
var handlerChain http.Handler = mux
handlerChain = middleware.CORSMiddleware(handlerChain)             // 4. Sets CORS headers
handlerChain = middleware.RequestIDMiddleware(handlerChain)        // 3. Generates UUID, sets X-Request-ID
handlerChain = middleware.TenantIdentifierMiddleware(baseDomain)(handlerChain)  // 2. Extracts "acme" from Host, sets X-Subdomain
handlerChain = tracing.Middleware(handlerChain)                   // 1. Creates OTel span

After this chain, request headers now include: X-Request-ID: <uuid>, X-Subdomain: acme, plus an active OpenTelemetry span.

Step 3: Gateway route matching & proxy

apps/gateway-service/internal/handler/router.go:29-49

The Router's ServeHTTP method matches the request path against routes.yaml using prefix matching.

func (rtr *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    route := rtr.findRoute(r.URL.Path)
    // Path "/api/v1/auth/login" matches route: path="/api/v1/auth"
    // Config: auth_required=false, target_url="http://localhost:8081"

    if route.AuthRequired {
        // SKIPPED for /api/v1/auth — auth-service handles its own auth
    }

    rtr.proxyRequest(w, r, route)  // → reverse proxy to http://localhost:8081
}
Key insight: The /api/v1/auth route has auth_required: false. This means the gateway does not validate JWTs for auth-service routes. The auth-service applies its own JWT middleware for protected endpoints like /users.

Step 4: Auth-service receives proxied request

apps/auth-service/cmd/server/main.go:133-138

The auth-service's Chi router matches the /login endpoint. This route sits outside the authenticated group, so no JWT middleware is applied.

r.Route("/api/v1/auth", func(r chi.Router) {
    r.Post("/login", authHandler.Login)          // ← matched! No auth required
    r.Post("/refresh-token", authHandler.RefreshToken)
    // ...
    r.Group(func(ar chi.Router) {
        ar.Use(auth.JWTMiddleware(cfg.JwtCfg))       // JWT required for this group
        ar.Use(auth.TenantContextFromClaimsMiddleware)
        ar.Route("/users", func(r chi.Router) { ... })
    })
})

Step 5: Handler decodes & validates

apps/auth-service/internal/handler/auth.go:20-46

The Login handler decodes JSON, validates fields, calls the service layer, and sets response cookies.

func (h *authHandler) Login(w http.ResponseWriter, r *http.Request) {
    var req model.AuthLogin
    if err := httpx.DecodeAndValidate(r, &req); err != nil {
        httpx.ValidationError(w, err)  // 400 with field-level errors
        return
    }

    resp, err := h.svc.Login(&req, r.Context())  // → Step 6
    if err != nil {
        httpx.HandleError(w, err)  // maps to HTTP status via typed errors
        return
    }

    httpx.SetRefreshTokenCookie(resp.RefreshToken, w)  // HttpOnly cookie
    httpx.SetTenantIdCookie(resp.TenantId, w)

    data := map[string]interface{}{
        "user_id":   resp.UserId,
        "token":     resp.Token,
        "tenant_id": tenantID,
        "subdomain": resp.Subdomain,
    }
    json.NewEncoder(w).Encode(data)
}

Step 6: Service — business logic

apps/auth-service/internal/service/auth_service.go:33-71

The service layer orchestrates the full login sequence: DB lookup, password verification, cross-service tenant fetch, and JWT generation.

func (s *authService) Login(p *model.AuthLogin, ctx context.Context) (*domain.LoginResponse, error) {
    user, err := s.repo.Login(p)                       // → Step 7: DB query
    if err != nil { return nil, err }

    err = bcrypt.CompareHashAndPassword(                // Verify password
        []byte(user.Password), []byte(p.Password))
    if err != nil {
        return nil, httpx.Unauthorized("invalid credentials")
    }

    if user.IsActive != nil && !*user.IsActive {
        return nil, httpx.Unauthorized("account is deactivated")
    }

    tenantData, err := s.GetTenantData(tenantId)       // → Step 8: HTTP → admin-service
    tokenStr, refreshToken, err := s.repo.GetAccessToken(user, ctx, tenantData)  // → Step 9

    return &domain.LoginResponse{
        UserId: user.ID.String(), Token: tokenStr,
        RefreshToken: refreshToken, TenantId: tenantId,
        Subdomain: tenantData["Subdomain"],
    }, nil
}

Step 7: Repository — database query

apps/auth-service/internal/repository/auth_repo.go:35-47

GORM query with eager loading. Preloads the entire role/permission tree in a single operation.

func (r *authRepository) Login(p *model.AuthLogin) (*model.User, error) {
    var u model.User
    if err := r.db.
        Preload("Role.RoleMenu.Menu").        // Eager load: User → Role → []RoleMenu → Menu
        Where("email = ? or phone = ?", p.Email, p.Email).
        First(&u).Error; err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, httpx.Unauthorized("invalid credentials")
        }
        return nil, err
    }
    return &u, nil
}
// SQL generated (approx):
// SELECT * FROM users WHERE email = $1 OR phone = $1 LIMIT 1
// SELECT * FROM roles WHERE id = $1
// SELECT * FROM role_menus WHERE role_id = $1
// SELECT * FROM menus WHERE id IN ($1, $2, ...)

Step 8: Cross-service call to admin-service

apps/auth-service/internal/service/auth_service.go:99-115

The auth-service calls admin-service over HTTP to fetch tenant metadata (subdomain, features). This is a synchronous service-to-service call.

func (s *authService) GetTenantData(tenantID string) (map[string]any, error) {
    url := s.internalUrl.AdminServiceUrl + "/me/tenants/" + tenantID
    // e.g. "http://localhost:8082/internal/me/tenants/abc-123"
    respAuth, err := httpx.DoJSON("GET", url, nil, "")

    var result map[string]any
    json.NewDecoder(respAuth.Body).Decode(&result)
    return result, nil  // { "Subdomain": "acme", "Features": [...], ... }
}
Cross-service boundary: This HTTP call goes from auth-service → admin-service. The admin-service endpoint /internal/me/tenants/{id} is registered as an internal route with no auth required (service-to-service trust).

Step 9: JWT generation + Redis sync

apps/auth-service/internal/repository/auth_repo.go:170-278

Generates the access token (JWT with claims), creates a refresh token (UUID hashed with SHA-256), and syncs version numbers to Redis for real-time invalidation.

func (r *authRepository) GenerateAccessToken(user, ctx, tenantData) (string, error) {
    // Build permission map from user's role menus
    permissions := map[string][]string{
        "broadcast": {"read", "create"},
        "users":     {"read", "create", "update", "delete"},
        // ... built from Role.RoleMenu.Menu + Can* flags
    }

    claims := jwt.MapClaims{
        "sub":               user.ID,
        "tenant_id":         user.TenantId,
        "role":              roleName,
        "subdomain":         tenantData["Subdomain"],
        "permissions":       permissions,
        "role_id":           user.RoleId,
        "token_version":     tokenVersion,
        "role_version":      roleVersion,
        "role_user_version": roleUserVersion,
        "tenant_feature":    tenantFeature,
        "exp":               time.Now().Add(r.accessTokenExpiry).Unix(),
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenStr, _ := token.SignedString([]byte(r.secret))

    // Sync versions to Redis for real-time invalidation
    redisclient.SyncAuthVersion(ctx, "auth:token_version:", userId, tokenVersion)
    redisclient.SyncAuthVersion(ctx, "auth:role_version:", roleId, roleVersion)
    redisclient.SyncAuthVersion(ctx, "auth:role_user_version:", userId, roleUserVersion)

    return tokenStr, nil
}

// Refresh token: UUID → SHA-256 hash → stored in DB
func (r *authRepository) AccessAndRefreshToken(...) {
    newRefresh := uuid.NewString()
    newHash := helpers.HashRefreshToken(newRefresh)
    newRT := model.RefreshToken{ID: uuid.New(), UserID: user.ID, TokenHash: newHash, ...}
    db.Create(&newRT)
}

Step 10: Response

The response flows back through the reverse proxy to the client.

// HTTP Response: 200 OK
// Set-Cookie: refresh_token=<uuid>; HttpOnly; Path=/
// Set-Cookie: tenant_id=<uuid>; Path=/
{
  "user_id": "550e8400-e29b-41d4-a716-446655440000",
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "tenant_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "subdomain": "acme"
}

8.3 TRACE 2: POST /api/v1/auth/users (Create User)

An authenticated flow that passes through gateway without JWT validation (auth-service handles its own), then through the auth-service's JWT middleware to create a new user.

StepDescription
Step 1 Client sends POST /api/v1/auth/users with Authorization: Bearer <token> and user JSON body.
Step 2 Gateway middleware chain executes: tracing.MiddlewareTenantIdentifierMiddlewareRequestIDMiddlewareCORSMiddleware. Same as login flow.
Step 3 Gateway matches /api/v1/authauth_required: false. The gateway does not validate the JWT; it proxies the request directly to auth-service at http://localhost:8081.
Step 4 Auth-service middleware: auth.JWTMiddleware(cfg.JwtCfg) validates the Bearer token, checks token signature (HS256), verifies expiration, compares token_version / role_version / role_user_version against Redis to detect revoked tokens. Injects decoded claims into request context.
apps/auth-service/cmd/server/main.go:152-154
Step 5 auth.TenantContextFromClaimsMiddleware extracts tenant_id, sub (user ID), role, and subdomain from JWT claims and builds a TenantContext struct stored in request context via context.WithValue().
Step 6 Handler (user.go:28-53): Decodes the User JSON body with httpx.DecodeAndValidate. Calls auth.RequireTenantContext(r) to get the authenticated user's context. If the caller is a Platform Admin and the body includes tenant_id, that tenant is honored. Otherwise, the user's own tc.TenantID is used.
Step 7 Service (user_service.go:60-85): Hashes password with bcrypt.GenerateFromPassword(DefaultCost), lowercases email with strings.ToLower, then calls repo.Create(user).
Step 8 Repository (user_repo.go:32-73): Executes a GORM transaction. First creates the User row. If the user has ConfigID (config options), creates ConfigUsers join records. Catches PG error code 23503 (foreign key violation) and returns a conflict error.
Step 9 Service post-processing: If SendMail flag is true, dispatches a set-password email via the background mail.EmailWorker channel. If BusinessNumberID is set, publishes a business_number.requested event to Kafka topic admin.events.v1 for the admin-service to consume.
Step 10 Response: 201 Created with the created user JSON (password field stripped).

8.4 TRACE 3: Webhook → Kafka → Worker

A complete asynchronous flow for WhatsApp message status updates. Meta sends a webhook, which gets validated, resolved to a tenant, published to Kafka, and consumed by the broadcast worker.

Step 1: Meta sends POST to webhook endpoint

Meta's WhatsApp Business API sends a webhook notification to our endpoint.

POST /api/v1/webhooks/meta
X-Hub-Signature-256: sha256=abc123def456...
Content-Type: application/json

{
  "object": "whatsapp_business_account",
  "entry": [{
    "id": "waba_123456789",
    "changes": [{
      "field": "messages",
      "value": {
        "statuses": [{ "id": "wamid.xxx", "status": "delivered", ... }]
      }
    }]
  }]
}

Step 2: meta-webhook-service handler

apps/meta-webhook-service/internal/handler/webhook_handler.go:86-138

The handler reads the raw body (required for HMAC verification), validates the signature, and parses the payload.

func (h *WebhookHandler) HandleWebhookEvent(w, r) {
    // 1. Read raw body for signature validation
    body, _ := io.ReadAll(r.Body)

    // 2. Validate HMAC-SHA256 signature with Meta app secret
    signature := r.Header.Get("X-Hub-Signature-256")
    if err := h.validator.Validate(signature, body); err != nil {
        http.Error(w, "Invalid signature", 401)
        return
    }

    // 3. Parse JSON payload
    var payload sharedModel.MetaWebhookPayload
    json.Unmarshal(body, &payload)

    // 4. Process each entry
    for _, entry := range payload.Entry {
        wabaID := entry.ID
        tenant, _ := h.tenantResolver.ResolveByWABAId(ctx, wabaID)  // → Step 3
        for _, change := range entry.Changes {
            h.processChange(ctx, tenant.TenantID, wabaID, &change, payload)  // → Step 4
        }
    }

    w.WriteHeader(200)  // Always return 200 to Meta
}

Step 3: Tenant resolution via admin-service

apps/meta-webhook-service/internal/service/tenant_resolver.go:40-68

Maps a WhatsApp Business Account (WABA) ID to a tenant ID by calling admin-service's internal endpoint.

func (r *TenantResolver) ResolveByWABAId(ctx, wabaID) (*TenantInfo, error) {
    url := fmt.Sprintf("%s/internal/waba/%s/tenant", r.adminServiceURL, wabaID)
    // → GET http://admin-service:8082/internal/waba/waba_123456789/tenant
    resp, _ := r.httpClient.Do(req)
    // Returns: { "tenant_id": "abc-123", "waba_id": "waba_123456789" }
}

Step 4: Event publishing to Kafka

apps/meta-webhook-service/internal/service/event_publisher.go:138-171

Based on the webhook field type, the publisher creates an event and publishes to the appropriate Kafka topic.

// Route by change.Field:
switch change.Field {
case "message_template_status_update":
    // → topic: whatsapp.template.status.v1
    // → event type: whatsapp.template.status_update.v1

case "messages":
    // Statuses → topic: whatsapp.message.status.v1
    // → event type: whatsapp.message.status.v1
    // Messages → topic: whatsapp.message.incoming.v1
}

// Event envelope structure:
event := eventbus.Event{
    ID:       uuid,
    Type:     "whatsapp.message.status.v1",
    Source:   "meta-webhook-service",
    TenantID: "abc-123",
    Data:     MessageStatusPayload{...},  // JSON-serialized
}

Step 5: Broadcast Worker consumes from Kafka

apps/broadcast-service/cmd/worker/main.go:266-276

The broadcast worker subscribes to multiple Kafka topics. Each message is deserialized into an Event envelope and routed to the appropriate handler.

// Worker subscribes to these topics:
consumer.Subscribe("broadcasts.v1", groupID, handler)
consumer.Subscribe("whatsapp.template.status.v1", groupID, metaTemplateHandler)
consumer.Subscribe("whatsapp.message.status.v1", groupID, metaMessageStatusHandler)
consumer.Subscribe("whatsapp.template.quality.v1", groupID, metaTemplateQualityHandler)

// Message status handler:
metaMessageStatusHandler := func(ctx, event) error {
    if event.Type == eventbus.EventTypeWhatsAppMessageStatus {
        metaConsumerSvc.HandleMessageStatusEvent(ctx, event)
        // Updates message delivery status in DB: sent/delivered/read/failed
    }
}

Step 6: Handler processes status update

The MetaConsumerService updates the database record with the new delivery status. For template status updates, it updates the template's approval/rejection status.

// Template status: update template status in DB
//   APPROVED → template is ready to use
//   REJECTED → template needs revision, reason stored

// Message status: update delivery status in broadcast_messages table
//   sent → delivered → read (progressive status)
//   failed → store error code and reason

Async Flow Sequence Diagram

  Meta               meta-webhook-svc     admin-svc        Kafka              broadcast-worker     DB
   │                      │                  │               │                      │              │
   │──POST /webhooks/meta→│                  │               │                      │              │
   │                      │                  │               │                      │              │
   │                      │ Validate HMAC    │               │                      │              │
   │                      │ Parse payload    │               │                      │              │
   │                      │                  │               │                      │              │
   │                      │──GET /internal/──→│               │                      │              │
   │                      │  waba/{id}/tenant │               │                      │              │
   │                      │←─{ tenant_id }───│               │                      │              │
   │                      │                  │               │                      │              │
   │                      │──Publish Event──────────────────→│                      │              │
   │                      │  topic: whatsapp.│               │                      │              │
   │                      │  message.status  │               │                      │              │
   │                      │                  │               │                      │              │
   │←──── 200 OK ─────────│                  │               │                      │              │
   │                      │                  │               │──Consume Event──────→│              │
   │                      │                  │               │                      │              │
   │                      │                  │               │                      │──UPDATE ────→│
   │                      │                  │               │                      │  msg status  │
   │                      │                  │               │                      │←─── OK ──────│
   │                      │                  │               │                      │              │

8.5 TRACE 4: Broadcast Message Sending Pipeline

The complete flow from campaign creation through WhatsApp delivery, covering multiple async stages.

StepDescription
Step 1 Create campaign: User sends POST /api/v1/broadcast/campaign with template, recipients group, and schedule. The BroadcastHandler creates a BroadcastCampaign record. If no approval is required and schedule is "immediately", a BCJob is dispatched to the in-process worker pool via dispatcher.TryDispatch().
apps/broadcast-service/internal/handler/broadcast_handler.go:80-143
Step 2 Upload recipients: User uploads CSV via POST /api/v1/broadcast/recipient-groups/upload. The handler uploads the file to S3, creates an upload job record, then publishes a recipient.upload.requested event to Kafka. The broadcast worker picks up this event and processes the CSV asynchronously: validates phone numbers, creates recipient records in batches.
Step 3 Trigger broadcast: If the campaign needs approval, the approver calls PATCH /api/v1/broadcast/approval/{id}. Once approved (or immediately if no approval required), the BroadcastMessageService generates individual BroadcastMessage records — one per recipient — and publishes each as a Kafka event to broadcasts.v1.
Step 4 Worker consumes messages: The broadcast worker consumes broadcast.message.requested events from Kafka using a semaphore-bounded goroutine pool (default 20 workers). Each message goes through a RetryableHandler with exponential backoff. A per-tenant rate limiter (default 70 RPS, burst 10) prevents exceeding Meta API limits.
apps/broadcast-service/cmd/worker/main.go:190-252
Step 5 Meta API call: The ConsumerMessageService fetches Meta credentials for the tenant (via admin-service), constructs the WhatsApp template message payload, and calls the Meta Cloud API. On success, the message status is set to "sent". On failure, error details are stored.
Step 6 Webhook status updates: Meta sends delivery receipts (sent → delivered → read) via webhooks. These flow through Trace 3 above. The broadcast worker updates each message's status progressively.

Broadcast Pipeline Sequence Diagram

  Client        broadcast-svc     Kafka          broadcast-worker    admin-svc    Meta API     meta-webhook
    │                │              │                  │                │            │              │
    │──POST /campaign→│              │                  │                │            │              │
    │←──campaign JSON─│              │                  │                │            │              │
    │                │              │                  │                │            │              │
    │──POST /upload──→│              │                  │                │            │              │
    │                │──Upload S3───│                  │                │            │              │
    │                │──Publish ───→│                  │                │            │              │
    │←──job_id───────│  upload.req  │                  │                │            │              │
    │                │              │──Consume────────→│                │            │              │
    │                │              │                  │──Process CSV───│            │              │
    │                │              │                  │  (validate,    │            │              │
    │                │              │                  │   batch insert)│            │              │
    │                │              │                  │                │            │              │
    │──PATCH /approve→│              │                  │                │            │              │
    │                │──Publish ───→│  per-recipient   │                │            │              │
    │←──approved─────│  msg events  │                  │                │            │              │
    │                │              │                  │                │            │              │
    │                │              │──Consume────────→│                │            │              │
    │                │              │  (20 workers,    │                │            │              │
    │                │              │   rate limited)  │                │            │              │
    │                │              │                  │──GET creds────→│            │              │
    │                │              │                  │←──meta token───│            │              │
    │                │              │                  │──POST message─────────────→│              │
    │                │              │                  │←──wamid.xxx────────────────│              │
    │                │              │                  │                │            │              │
    │                │              │                  │                │            │──webhook────→│
    │                │              │                  │                │            │  (delivered)  │
    │                │              │                  │                │            │              │
    │                │              │  ←──status event──────────────────────────────│              │
    │                │              │──Consume────────→│                │            │              │
    │                │              │                  │──UPDATE status─│            │              │
    │                │              │                  │                │            │              │

8.6 Middleware Execution Order

Each service applies its own middleware chain. Middleware wraps handlers, so the last applied middleware executes first (outermost). Below is the exact execution order for each service.

gateway-service Middleware Chain

Applied in apps/gateway-service/cmd/server/main.go:130-134:

OrderMiddlewareDescription
1tracing.MiddlewareCreates OpenTelemetry span, propagates trace context
2TenantIdentifierMiddlewareExtracts subdomain from Host header, sets X-Subdomain
3RequestIDMiddlewareGenerates UUID, sets X-Request-ID header
4CORSMiddlewareSets Access-Control-* CORS headers
5Router.ServeHTTPRoute matching, optional JWT validation, reverse proxy
Gateway JWT validation: If a matched route has auth_required: true, the Router additionally validates the JWT, extracts claims, and injects X-Tenant-ID, X-User-ID, X-User-Role, and X-Subdomain headers before proxying. Role-based access control is also checked at this level.

auth-service Middleware Chain

Applied in apps/auth-service/cmd/server/main.go:126-184:

OrderMiddlewareDescription
1tracing.MiddlewareWraps entire Chi router (applied at server level)
2chimiddleware.LoggerLogs request method, path, duration
3middleware.RequestIDMiddlewareEnsures X-Request-ID is present

For authenticated routes (inside r.Group), two additional middleware layers apply:

OrderMiddlewareDescription
4auth.JWTMiddlewareValidates JWT signature, expiry, Redis version checks
5auth.TenantContextFromClaimsMiddlewareExtracts TenantContext from JWT claims into context

admin-service Middleware Chain

Applied in apps/admin-service/cmd/server/main.go:120-217:

OrderMiddlewareDescription
1tracing.MiddlewareWraps entire Chi router (applied at server level)
2chimiddleware.LoggerRequest logging
3middleware.RequestIDMiddlewareRequest ID propagation

For /api/v1 routes (except OAuth callback):

OrderMiddlewareDescription
4auth.TenantContextMiddlewareReads X-Tenant-ID, X-User-ID, X-User-Role headers (set by gateway)

For /api/v1/admin routes:

OrderMiddlewareDescription
5auth.RequireRole("Platform Admin")Rejects non-admin requests with 403

broadcast-service Middleware Chain

Applied in apps/broadcast-service/cmd/server/main.go:144-171:

OrderMiddlewareDescription
1chimiddleware.LoggerRequest logging
2chimiddleware.RequestIDRequest ID (chi built-in)
3chimiddleware.RecovererPanic recovery, returns 500
4tracing.MiddlewareOpenTelemetry tracing
5auth.HybridAuthMiddlewareAccepts either JWT Bearer token OR X-Tenant-ID/X-User-ID headers (from gateway). Builds TenantContext from either source.

meta-webhook-service Middleware Chain

Applied in apps/meta-webhook-service/cmd/server/main.go:82-86:

OrderMiddlewareDescription
1chimiddleware.LoggerRequest logging
2middleware.RequestIDMiddlewareRequest ID propagation
3chimiddleware.RecovererPanic recovery
4tracing.MiddlewareOpenTelemetry tracing
No auth middleware: The meta-webhook-service doesn't use JWT or tenant context middleware. Authentication is handled by HMAC-SHA256 signature verification within the handler itself, using the Meta app secret.

8.7 Complete API Endpoint Reference

All registered routes across all services, showing the method, path, authentication requirement, and description.

gateway-service Route Configuration

Defined in apps/gateway-service/config/routes.yaml. These are prefix-matched proxy routes.

Path PrefixTargetAuthRolesNotes
/healthlocalhost:8080No-Gateway health check
/api/v1/authlocalhost:8081No-Auth-service handles its own auth
/api/v1/adminlocalhost:8082YesPlatform AdminAdmin-only routes
/api/v1/tenantslocalhost:8082Yes-Tenant management
/api/v1/oauthlocalhost:8082No-OAuth callbacks
/api/v1/broadcastlocalhost:8083Yes-Broadcast service
/internallocalhost:8082No-Service-to-service (internal_only)

auth-service :8081

Defined in apps/auth-service/cmd/server/main.go:130-181

MethodPathAuthDescription
GET/internal/users/{id}NoInternal: get user by ID
GET/api/v1/auth/NoService hello
GET/api/v1/auth/helloNoHello endpoint
POST/api/v1/auth/loginNoUser login (email + password)
POST/api/v1/auth/refresh-tokenNoRefresh access token (cookie-based)
POST/api/v1/auth/reset-passwordNoReset password with code
POST/api/v1/auth/users/sendmail-resetpassNoSend password reset email
GET/api/v1/auth/healthNoHealth check
GET/api/v1/auth/mypermissionJWTGet current user's permissions
POST/api/v1/auth/usersJWTCreate user
GET/api/v1/auth/usersJWTList users for tenant
GET/api/v1/auth/users/approvers/countJWTCount approver users
GET/api/v1/auth/users/{id}JWTGet user by ID
DELETE/api/v1/auth/users/{id}JWTDelete user
PUT/api/v1/auth/users/{id}JWTUpdate user
POST/api/v1/auth/users/change-passwordJWTChange own password
POST/api/v1/auth/rolesJWTCreate role
POST/api/v1/auth/roles/permissionJWTCreate role permission
PUT/api/v1/auth/roles/permission/{role_id}JWTUpsert role permissions
POST/api/v1/auth/roles/menuJWTCreate role-menu mapping
GET/api/v1/auth/rolesJWTList roles
GET/api/v1/auth/roles/{id}JWTGet role by ID
DELETE/api/v1/auth/roles/{id}JWTDelete role
POST/api/v1/auth/internal/setup-tenant-rolePlatform AdminSet up default role for new tenant
POST/api/v1/auth/menusJWTCreate menu
GET/api/v1/auth/menusJWTList menus
GET/api/v1/auth/menus/{id}JWTGet menu by ID

admin-service :8082

Defined in apps/admin-service/cmd/server/main.go:120-214

MethodPathAuthDescription
Internal (service-to-service)
GET/internal/tenants/{id}/meta-credentialsNoGet Meta credentials for tenant
GET/internal/waba/meta-credentialsNoGet all Meta credentials
GET/internal/waba/{waba_id}/tenantNoResolve WABA ID to tenant
GET/internal/me/tenantsNoGet tenant data (service-to-service)
GET/internal/me/tenants/{id}NoGet specific tenant data
GET/internal/me/business-number/{phone}/{tenant_id}NoGet business number by phone
Public
GET/api/v1/oauth/meta/callbackNoMeta OAuth callback
Tenant Management (auth via gateway headers)
POST/api/v1/tenantsHeadersCreate tenant
GET/api/v1/tenantsHeadersList tenants
PUT/api/v1/tenants/{id}HeadersUpdate tenant
GET/api/v1/tenants/{id}HeadersGet tenant by ID
DELETE/api/v1/tenants/{id}HeadersDelete tenant
POST/api/v1/tenants/create-uploadHeadersCreate upload for tenant
GET/api/v1/tenants/me/meta/oauth-urlHeadersGenerate Meta OAuth URL
GET/api/v1/tenants/me/meta/statusHeadersGet Meta connection status
PUT/api/v1/tenants/me/meta/primary-phoneHeadersSet primary WhatsApp phone
DELETE/api/v1/tenants/me/meta/disconnectHeadersDisconnect WhatsApp
GET/api/v1/tenants/me/business-numberHeadersList business numbers
GET/api/v1/tenants/me/business-number/myHeadersGet my business numbers
POST/api/v1/tenants/me/business-numberHeadersCreate business number
PUT/api/v1/tenants/me/business-number/{id}HeadersUpdate business number
GET/api/v1/tenants/me/business-number/{id}HeadersGet business number by ID
DELETE/api/v1/tenants/me/business-number/{id}HeadersDelete business number
GET/api/v1/tenants/me/settingsHeadersGet tenant settings
PATCH/api/v1/tenants/me/settingsHeadersUpdate tenant settings
Admin-Only (Platform Admin role required)
GET/api/v1/admin/tenants/{id}/meta-settingsPlatform AdminGet tenant Meta settings
PUT/api/v1/admin/tenants/{id}/meta-settingsPlatform AdminUpdate Meta settings
POST/api/v1/admin/tenants/{id}/meta-settings/testPlatform AdminTest Meta connection
DELETE/api/v1/admin/tenants/{id}/meta-settingsPlatform AdminDelete Meta settings
POST/api/v1/admin/tenants/{id}/meta-settings/refresh-tokenPlatform AdminRefresh Meta token
POST/api/v1/admin/tenants/{id}/meta-settings/sync-numbersPlatform AdminSync phone numbers from Meta
GET/api/v1/admin/tenants/{id}/meta-status/refreshPlatform AdminRefresh Meta status
GET/api/v1/admin/tenants/{id}/business-numbersPlatform AdminAdmin: list business numbers
POST/api/v1/admin/tenants/{id}/business-numbersPlatform AdminAdmin: create business number
PUT/api/v1/admin/tenants/{id}/business-numbers/{bn_id}Platform AdminAdmin: update business number
GET/api/v1/admin/tenants/{id}/business-numbers/{bn_id}Platform AdminAdmin: get business number
DELETE/api/v1/admin/tenants/{id}/business-numbers/{bn_id}Platform AdminAdmin: delete business number
GET/api/v1/admin/alertsPlatform AdminGet active platform alerts
PUT/api/v1/admin/alerts/{id}/acknowledgePlatform AdminAcknowledge alert
PUT/api/v1/admin/alerts/{id}/resolvePlatform AdminResolve alert
POST/api/v1/admin/alertsPlatform AdminCreate alert

broadcast-service :8083

Defined in apps/broadcast-service/cmd/server/main.go and handler files. Uses Huma API framework.

MethodPathAuthDescription
Broadcast Campaigns
POST/api/v1/broadcast/campaignHybridCreate broadcast campaign
GET/api/v1/broadcast/campaignHybridList campaigns (paginated)
GET/api/v1/broadcast/campaign/{id}HybridGet campaign by ID
PUT/api/v1/broadcast/campaign/{id}HybridUpdate campaign
DELETE/api/v1/broadcast/campaign/{id}HybridDelete campaign
POST/api/v1/broadcast/campaign-send/{id}HybridSend/trigger broadcast campaign
PATCH/api/v1/broadcast/approval/{id}HybridApprove or reject campaign
Broadcast Reports
GET/api/v1/broadcast/campaign/{id}/reportHybridGet campaign delivery report
GET/api/v1/broadcast/campaign/{id}/messagesHybridList campaign messages (paginated)
GET/api/v1/broadcast/campaign/{id}/report/downloadHybridDownload campaign report as CSV
GET/api/v1/broadcast/messages/{id}HybridSend one test message via Meta
Recipient Groups
POST/api/v1/broadcast/recipient-groupsHybridCreate recipient group
GET/api/v1/broadcast/recipient-groupsHybridList recipient groups
GET/api/v1/broadcast/recipient-groups/{id}HybridGet group details
PATCH/api/v1/broadcast/recipient-groups/{id}HybridUpdate group
DELETE/api/v1/broadcast/recipient-groups/{id}HybridDelete group
GET/api/v1/broadcast/recipient-groups/{id}/exportHybridExport recipients as CSV
GET/api/v1/broadcast/recipient-groups/variable/{groupId}HybridGet group variable mappings
Recipients
GET/api/v1/broadcast/recipient-groups/{id}/recipientsHybridList recipients in group
POST/api/v1/broadcast/recipient-groups/{group_id}/recipientsHybridAdd recipient to group
GET/api/v1/broadcast/recipient-groups/{groupId}/recipients/{id}HybridGet recipient details
PATCH/api/v1/broadcast/recipient-groups/{groupId}/recipients/{id}HybridUpdate recipient
DELETE/api/v1/broadcast/recipient-groups/{groupId}/recipients/{id}HybridDelete recipient
Upload
POST/api/v1/broadcast/recipient-groups/uploadHybridUpload recipients CSV (async)
GET/api/v1/broadcast/recipient-groups/upload-templateHybridDownload CSV template
GET/api/v1/broadcast/recipient-groups/{groupId}/upload-status/{jobId}HybridGet upload job status
WhatsApp Templates
GET/api/v1/broadcast/whatsapp-templatesHybridList templates (filterable)
GET/api/v1/broadcast/whatsapp-templates/{id}HybridGet template by ID
POST/api/v1/broadcast/whatsapp-templatesHybridCreate template
DELETE/api/v1/broadcast/whatsapp-templates/{id}HybridDelete template
POST/api/v1/broadcast/whatsapp-templates/{id}/duplicateHybridDuplicate template
POST/api/v1/broadcast/whatsapp-templates/{id}/submitHybridSubmit template for Meta review
POST/api/v1/broadcast/whatsapp-templates/media/uploadHybridUpload template media to S3
GET/api/v1/broadcast/whatsapp-templates/meta-example/{id}HybridGet Meta format template
GET/api/v1/broadcast/whatsapp-templates/meta-media/{tenant_id}HybridGet Meta media ID
GET/api/v1/broadcast/whatsapp-templates/sync-quality/{tenant_id}HybridSync template quality from Meta
Broadcast Labels
GET/api/v1/broadcast/labelsHybridList broadcast labels
POST/api/v1/broadcast/labelsHybridCreate broadcast label

meta-webhook-service :8084

Defined in apps/meta-webhook-service/cmd/server/main.go:82-111

MethodPathAuthDescription
GET/api/v1/webhooks/metaHMACMeta webhook verification (challenge-response)
POST/api/v1/webhooks/metaHMACReceive Meta webhook events (signature-verified)
GET/healthNoHealth check
Auth column legend:
No = No authentication.
JWT = Bearer token validated by auth-service JWTMiddleware.
Headers = X-Tenant-ID/X-User-ID headers injected by gateway.
Hybrid = Accepts either JWT token or gateway-injected headers.
Platform Admin = JWT + Platform Admin role required.
HMAC = HMAC-SHA256 signature verification with Meta app secret.

1Engage Multitenant Backend — Codebase Guide — Built for developers who need to understand the codebase quickly.