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 :
Trigger : POST to /analyze-rfp returns immediately with status: "processing"
Lambda Invocation : Backend invokes AnalysisWorkerFunction with InvocationType: "Event"
Polling : Frontend polls GET /analysis-status every 3 seconds
Completion : Status changes to "completed" with analysis results
Request
The proposal ID or code (format: PROP-YYYYMMDD-XXXX)
Response
processing: Analysis started successfully
completed: Analysis already exists (cached)
Instruction to poll the status endpoint
ISO 8601 timestamp when analysis started
true if returning cached results (no re-analysis)
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:
Status Description 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
Response
Current analysis status: not_started, processing, completed, or failed
Analysis results (only when status is completed)
ISO timestamp (only when completed)
ISO timestamp (only when processing)
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.