A webhook ingestion service built with FastAPI that receives, validates, and stores SMS messages with HMAC signature verification.
| Category | Technology |
|---|---|
| Language | Python 3.11 |
| Framework | FastAPI |
| Validation | Pydantic v2 |
| Database | SQLite (via SQLModel) |
| ORM | SQLModel (SQLAlchemy wrapper) |
| Container | Docker & Docker Compose |
| Testing | pytest |
- Docker & Docker Compose installed
- Git
# Clone the repository
git clone https://github.com/pushpitkamboj/lyftrAI_assignment.git
cd lyftrAI_assignment
# Set environment variables
export WEBHOOK_SECRET="your-secret-key"
export DATABASE_URL="sqlite:////data/app.db"
# Start the application
make upThat's it! The API will be running at http://localhost:8000 π
VSCode + GitHub Copilot + Occasional Prompts
Prompt for Containerizing the App
β Runs via Docker Compose using SQLite for storage.
DB: SQLite only; DB file must live under a Docker volume (e.g. /data/app.db)
understand the code and help me create 3 files
dockerfile
docker-compose.yml
makefile
Makefile targets:
β make up β docker compose up -d --build
β make down β docker compose down -v
β make logs β docker compose logs -f api
β make test β run your tests
understand the code #file:app and write the files
Prompt for README File
create a readme file which is divided in sections
technologies used -
language = python
validation = pydantic
container = docker
testing = pytest
framework = fastapi
etc
then complete setup workflow
git clone lyftrAI_assignment
cd
then run make up (it will do things for u :)
then run endpoints
GET
POST
whatever 4 endpoints i have ...
u can also see the logs by running make logs if u want to...
and then explain design decisions
1. focus on explaining implementation of HMAC verification in webhook
2. how pagination works (focus of /messages route functionality and telling the validation things and etc)
3. explaining logging style in app, running tests with make test
and then at last write contributing guidelines, write testcases etc basic stuff
| Command | Description |
|---|---|
make up |
Build and start containers in detached mode |
make down |
Stop containers and remove volumes |
make logs |
Stream logs from the API container (Ctrl+C to exit) |
make test |
Run pytest inside the container |
Liveness probe - checks if the service is running.
curl http://localhost:8000/health/liveResponse: 200 OK - "live route working fine"
Readiness probe - checks if the service is ready to accept traffic (WEBHOOK_SECRET set + DB accessible).
curl http://localhost:8000/health/readyResponse:
200 OK- "ready route working fine"503 Service Unavailable- if not ready
Receives and stores incoming SMS messages with HMAC signature verification.
# Generate HMAC signature
BODY='{"message_id":"msg-001","from":"+1234567890","to":"+0987654321","ts":"2025-12-28T10:00:00Z","text":"Hello!"}'
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}')
# Send request
curl -X POST http://localhost:8000/webhook \
-H "Content-Type: application/json" \
-H "X-Signature: $SIGNATURE" \
-d "$BODY"Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
message_id |
string | Yes | Unique message identifier |
from |
string | Yes | Sender phone (E.164 format: +<digits>) |
to |
string | Yes | Recipient phone (E.164 format) |
ts |
string | Yes | ISO-8601 UTC timestamp ending with Z |
text |
string | No | Message content (max 4096 chars) |
Response: 200 OK - {"status": "ok"}
Errors:
401 Unauthorized- Invalid/missing signature422 Unprocessable Entity- Validation error
Retrieve stored messages with pagination and filtering.
# Basic request
curl http://localhost:8000/messages
# With pagination
curl "http://localhost:8000/messages?limit=10&offset=0"
# Filter by sender
curl "http://localhost:8000/messages?from=%2B1234567890"
# Filter by timestamp
curl "http://localhost:8000/messages?since=2025-12-01T00:00:00Z"
# Search by text
curl "http://localhost:8000/messages?q=hello"Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 50 | Results per page (1-100) |
offset |
int | 0 | Number of records to skip |
from |
string | - | Filter by sender phone |
since |
string | - | Filter messages after timestamp |
q |
string | - | Search text content (case-insensitive) |
Response:
{
"data": [...],
"total": 100,
"limit": 50,
"offset": 0
}Get aggregated statistics about stored messages.
curl http://localhost:8000/statsResponse:
{
"total_messages": 150,
"senders_count": 25,
"messages_per_sender": [
{"from": "+1234567890", "count": 15},
...
],
"first_message_ts": "2025-12-01T08:00:00Z",
"last_message_ts": "2025-12-28T15:30:00Z"
}The webhook endpoint implements HMAC-SHA256 signature verification to ensure message authenticity and integrity.
How it works:
# 1. Read raw request body
body_bytes = await request.body()
# 2. Compute expected signature using shared secret
expected_signature = hmac.new(
key=os.getenv("WEBHOOK_SECRET").encode(),
msg=body_bytes,
digestmod=hashlib.sha256
).hexdigest()
# 3. Compare with provided signature (timing-attack safe)
if not hmac.compare_digest(expected_signature, x_signature):
raise HTTPException(status_code=401, detail="invalid_signature")Key Points:
- Uses
hmac.compare_digest()for constant-time comparison (prevents timing attacks) - Signature is computed over raw bytes (not parsed JSON) to catch any tampering
- Secret is loaded from environment variable (never hardcoded)
- Missing or invalid signatures return
401 Unauthorized
The messages endpoint provides flexible querying with offset-based pagination.
Pagination:
limit- Controls page size (default: 50, max: 100)offset- Skip N records for pagination- Response includes
totalcount for calculating pages
Filtering Options:
from- Exact match on sender phone numbersince- Messages with timestamp >= valueq- Case-insensitive text search using SQLILIKE
Validation:
- Phone numbers must be E.164 format (
+followed by digits) - Timestamps must be valid ISO-8601 UTC format
limitbounded between 1-100 to prevent abuseoffsetmust be non-negative
Ordering:
- Results sorted by
tsascending, thenmessage_idascending - Ensures consistent pagination results
All requests are logged in structured JSON format via middleware for easy parsing and analysis.
Log Format:
{
"ts": "2025-12-28T10:30:00.000000+00:00",
"level": "INFO",
"request_id": "uuid-here",
"method": "POST",
"path": "/webhook",
"status": 200,
"latency_ms": 12.34,
"result": "created",
"dup": false,
"message_id": "msg-001"
}Features:
- Request ID: Unique UUID per request for tracing
- Latency tracking: Response time in milliseconds
- Log level: INFO for success (< 400), ERROR for failures
- Contextual data: Webhook-specific fields (result, dup, message_id)
View Logs:
make logsTests are run inside the Docker container using pytest:
make testThis executes docker compose exec api pytest tests/ -v which:
- Runs pytest with verbose output
- Uses the same environment as the running application
- Tests against the SQLite database
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Make your changes
- Run tests:
make test - Commit:
git commit -m "Add my feature" - Push:
git push origin feature/my-feature - Open a Pull Request
Tests are located in the tests/ directory. Use pytest fixtures and FastAPI's TestClient:
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_health_live():
response = client.get("/health/live")
assert response.status_code == 200
def test_webhook_invalid_signature():
response = client.post(
"/webhook",
json={"message_id": "1", "from": "+123", "to": "+456", "ts": "2025-01-01T00:00:00Z"},
headers={"X-Signature": "invalid"}
)
assert response.status_code == 401