Skip to main content
POST
/
api
/
proposals
/
{proposal_id}
/
analyze-rfp
Analyze RFP
curl --request POST \
  --url https://api.example.com/api/proposals/{proposal_id}/analyze-rfp
{
  "status": "<string>",
  "message": "<string>",
  "started_at": "<string>",
  "cached": true,
  "rfp_analysis": {},
  "completed_at": "<string>",
  "error": "<string>"
}

Overview

Triggers AI analysis of the uploaded Request for Proposal (RFP) document. This is the first step in the proposal workflow. The endpoint follows an async pattern: it returns immediately after starting the analysis, and you must poll the status endpoint for completion.

Workflow Pattern

This endpoint uses an asynchronous Lambda worker pattern:
  1. Trigger: POST to /analyze-rfp returns immediately with status: "processing"
  2. Lambda Invocation: Backend invokes AnalysisWorkerFunction with InvocationType: "Event"
  3. Polling: Frontend polls GET /analysis-status every 3 seconds
  4. Completion: Status changes to "completed" with analysis results

Request

proposal_id
string
required
The proposal ID or code (format: PROP-YYYYMMDD-XXXX)

Response

status
string
  • processing: Analysis started successfully
  • completed: Analysis already exists (cached)
message
string
Instruction to poll the status endpoint
started_at
string
ISO 8601 timestamp when analysis started
cached
boolean
true if returning cached results (no re-analysis)
rfp_analysis
object
RFP analysis data (only present if cached/completed)

Example Request

curl -X POST "https://api.igad-innovation.org/api/proposals/PROP-20260304-A1B2/analyze-rfp" \
  -H "Authorization: Bearer YOUR_TOKEN"

Example Response

First Call (Processing)

{
  "status": "processing",
  "message": "RFP analysis started. Poll /analysis-status for completion.",
  "started_at": "2026-03-04T10:30:00.000Z"
}

Subsequent Call (Cached)

{
  "status": "completed",
  "rfp_analysis": {
    "semantic_query": "Build a digital platform for farmer cooperatives...",
    "key_requirements": [...],
    "evaluation_criteria": [...]
  },
  "message": "RFP already analyzed",
  "cached": true
}

Status Values

The analysis_status_rfp field in DynamoDB tracks the analysis state:
StatusDescription
not_startedNo analysis has been triggered
processingLambda worker is analyzing the RFP
completedAnalysis finished successfully
failedAnalysis encountered an error

Polling for Status

After triggering the analysis, poll the status endpoint:

GET /api/proposals/{proposal_id}/analysis-status

Check RFP analysis completion status

Polling Example

const pollStatus = async (proposalId: string) => {
  const interval = setInterval(async () => {
    const response = await fetch(
      `/api/proposals/${proposalId}/analysis-status`,
      { headers: { Authorization: `Bearer ${token}` } }
    )
    const data = await response.json()

    if (data.status === 'completed') {
      clearInterval(interval)
      console.log('Analysis complete:', data.rfp_analysis)
    } else if (data.status === 'failed') {
      clearInterval(interval)
      console.error('Analysis failed:', data.error)
    }
  }, 3000) // Poll every 3 seconds
}

Lambda Worker Details

Environment Variables

worker_function_arn = os.environ.get("WORKER_FUNCTION_NAME")
# Example: "arn:aws:lambda:us-east-1:123456789012:function:AnalysisWorkerFunction"

Lambda Invocation

lambda_client.invoke(
    FunctionName=worker_function_arn,
    InvocationType="Event",  # Async invocation (non-blocking)
    Payload=json.dumps({
        "proposal_id": proposal_code,  # Uses PROP-YYYYMMDD-XXXX format
        "analysis_type": "rfp"
    })
)

DynamoDB Updates

Before Lambda invocation:
await db_client.update_item(
    pk=f"PROPOSAL#{proposal_code}",
    sk="METADATA",
    update_expression="SET analysis_status_rfp = :status, rfp_analysis_started_at = :started",
    expression_attribute_values={
        ":status": "processing",
        ":started": datetime.utcnow().isoformat()
    }
)
After Lambda completes (in worker):
db_client.update_item_sync(
    pk=f"PROPOSAL#{proposal_code}",
    sk="METADATA",
    update_expression="SET analysis_status_rfp = :status, rfp_analysis = :result, rfp_analysis_completed_at = :completed",
    expression_attribute_values={
        ":status": "completed",
        ":result": analysis_result,
        ":completed": datetime.utcnow().isoformat()
    }
)

Caching Behavior

No Re-analysis: If rfp_analysis already exists in DynamoDB, the endpoint returns cached data immediately without triggering a new analysis.
This prevents:
  • Duplicate Lambda invocations
  • Unnecessary AI API costs
  • Wasted processing time

Error Handling

Status Code 400

{
  "detail": "Proposal code not found"
}

Status Code 403

{
  "detail": "Access denied"
}

Status Code 404

{
  "detail": "Proposal not found"
}

Status Code 500

{
  "detail": "RFP analysis failed: WORKER_FUNCTION_NAME environment variable not set"
}

GET Analysis Status

Description

Poll this endpoint to check the RFP analysis completion status.

Request

proposal_id
string
required
The proposal ID or code

Response

status
string
Current analysis status: not_started, processing, completed, or failed
rfp_analysis
object
Analysis results (only when status is completed)
completed_at
string
ISO timestamp (only when completed)
started_at
string
ISO timestamp (only when processing)
error
string
Error message (only when failed)

Example Response (Completed)

{
  "status": "completed",
  "rfp_analysis": {
    "semantic_query": "Develop a digital platform for agricultural cooperatives in the IGAD region...",
    "key_requirements": [
      "Mobile-first design",
      "Offline functionality",
      "Multi-language support"
    ],
    "evaluation_criteria": [
      {
        "criterion": "Technical Approach",
        "weight": "30%",
        "description": "Quality and feasibility of proposed solution"
      }
    ],
    "budget_range": "$50,000 - $100,000",
    "timeline": "12 months"
  },
  "completed_at": "2026-03-04T10:32:45.000Z"
}

Example Response (Processing)

{
  "status": "processing",
  "started_at": "2026-03-04T10:30:00.000Z"
}

Example Response (Failed)

{
  "status": "failed",
  "error": "Failed to extract text from RFP document"
}

Best Practices

Recommended Polling Strategy:
  • Interval: 3 seconds
  • Timeout: 5 minutes
  • Show loading indicator with elapsed time
  • Handle both success and failure states
Check for cached results before showing loading UI - the endpoint may return completed data immediately if analysis already exists.

Build docs developers (and LLMs) love