Skip to main content

Overview

All TPO (Time/Place/Occasion) endpoints are located under /api/tpo/. The API uses:
  • REST architecture with JSON payloads
  • Internal API key authentication for admin endpoints (via x-internal-api-key header)
  • Surge webhook signature validation for SMS webhooks

Public Endpoints

POST /api/tpo/signup

Initiates user onboarding via SMS. Authentication: None (public)
phoneNumber
string
required
E.164 format phone number (e.g., +12125551234)
success
boolean
Whether signup succeeded
message
string
Error message if signup failed

Request Example

curl -X POST https://josh.example.com/api/tpo/signup \
  -H "Content-Type: application/json" \
  -d '{"phoneNumber": "+12125551234"}'

Response Examples

{
  "success": true
}

Source Code

Location: src/app/api/tpo/signup/route.ts:9
export async function POST(req: NextRequest) {
  const body = await req.json();
  const { phoneNumber } = body;

  if (!/^\+1\d{10}$/.test(phoneNumber)) {
    return NextResponse.json(
      { message: "Invalid phone number. Expected E.164 format (+1XXXXXXXXXX)." },
      { status: 400 }
    );
  }

  await db.tpoUser.create({
    data: {
      phoneNumber,
      status: "ONBOARDING",
      onboardingStep: "AWAITING_ABOUT",
      onboardingQuestionIndex: 0,
    },
  });

  await sendSms(phoneNumber, TPO_INTRO_TEXT);
  // ... continues with first question
}

POST /api/tpo/webhook

Receives SMS messages from Surge API. Handles onboarding, scheduling, and message relay. Authentication: Surge signature validation Headers:
  • surge-signature - HMAC signature for request validation
Body: Surge webhook payload (see Surge API docs)

Response

{ "ok": true }

Source Code

Location: src/app/api/tpo/webhook/route.ts:958

Admin Endpoints

All admin endpoints require the x-internal-api-key header.

GET /api/tpo/admin/users

Fetches users with optional status filter. Authentication: Internal API key
status
TpoUserStatus
Filter by status: ONBOARDING, PENDING_REVIEW, APPROVED, REJECTED, BANNED
users
TpoUser[]
Array of user objects

Request Example

curl https://josh.example.com/api/tpo/admin/users?status=PENDING_REVIEW \
  -H "x-internal-api-key: your-secret-key"

Response Example

{
  "users": [
    {
      "id": "clx123abc",
      "phoneNumber": "+12125551234",
      "status": "PENDING_REVIEW",
      "onboardingStep": "COMPLETE",
      "aboutMe": "Q: tell us about yourself.\nA: I'm a software engineer...",
      "city": "New York",
      "photoUrls": ["photos/2125551234/1234567890.jpg"],
      "dlName": "John Doe",
      "dlAge": 28,
      "createdAt": "2026-03-01T12:00:00Z"
    }
  ]
}
Source: src/app/api/tpo/admin/users/route.ts:8

POST /api/tpo/admin/review

Approves or rejects a pending user. Authentication: Internal API key
userId
string
required
User ID to review
action
string
required
Either approve or reject
success
boolean
Whether action succeeded
status
string
New user status: APPROVED or REJECTED

Request Example

curl -X POST https://josh.example.com/api/tpo/admin/review \
  -H "Content-Type: application/json" \
  -H "x-internal-api-key: your-secret-key" \
  -d '{
    "userId": "clx123abc",
    "action": "approve"
  }'

Response Example

{
  "success": true,
  "status": "APPROVED"
}
Source: src/app/api/tpo/admin/review/route.ts:10

POST /api/tpo/admin/pair

Creates a matched pair and initiates scheduling. Authentication: Internal API key
userAId
string
required
First user ID (must be APPROVED)
userBId
string
required
Second user ID (must be APPROVED)
success
boolean
Whether pairing succeeded
dateId
string
Created TpoDate ID

Request Example

curl -X POST https://josh.example.com/api/tpo/admin/pair \
  -H "Content-Type: application/json" \
  -H "x-internal-api-key: your-secret-key" \
  -d '{
    "userAId": "clx123abc",
    "userBId": "clx456def"
  }'

Response Example

{
  "success": true,
  "dateId": "clx789ghi"
}
Source: src/app/api/tpo/admin/pair/route.ts:10

POST /api/tpo/admin/user-action

Performs administrative actions on a user. Authentication: Internal API key
userId
string
required
Target user ID
action
string
required
One of:
  • delete - Permanently delete user and their dates
  • ban - Set status to BANNED and end active dates
  • restart_onboarding - Reset user to beginning of onboarding
  • ping_onboarding - Send reminder SMS about current step
  • cancel_onboarding - Delete user record (for incomplete onboarding)
  • reparse_profile - Re-run AI profile structuring
  • edit_badge - Update a specific profile field
  • delete_badge - Remove a specific profile field
section
string
For badge actions: about or preferences
field
string
For badge actions: field name (e.g., hobbies, city, age)
currentValue
string
For badge actions: current value to replace
nextValue
string
For edit_badge: new value (omit for delete_badge)

Request Examples

{
  "userId": "clx123abc",
  "action": "ban"
}

Response Examples

{
  "success": true,
  "status": "BANNED"
}
Source: src/app/api/tpo/admin/user-action/route.ts:245

GET /api/tpo/admin/dates

Fetches all dates with optional status filter. Authentication: Internal API key
status
TpoDateStatus
Filter by status: ACTIVE or ENDED
dates
object[]
Array of date objects with user details and message count

Request Example

curl https://josh.example.com/api/tpo/admin/dates?status=ACTIVE \
  -H "x-internal-api-key: your-secret-key"

Response Example

{
  "dates": [
    {
      "id": "clx789ghi",
      "createdAt": "2026-03-02T10:00:00Z",
      "status": "ACTIVE",
      "schedulingPhase": "AGREED",
      "proposedSlot": "Friday, March 7th at 7pm",
      "agreedTime": "Friday, March 7th at 7pm",
      "suggestedPlace": "Cafe Maud in East Village, New York — cozy but lively, great for first dates",
      "portalEnabled": true,
      "userA": {
        "id": "clx123abc",
        "phoneNumber": "+12125551234",
        "city": "New York",
        "dlName": "John Doe",
        "dlAge": 28
      },
      "userB": {
        "id": "clx456def",
        "phoneNumber": "+12125555678",
        "city": "Brooklyn",
        "dlName": "Jane Smith",
        "dlAge": 26
      },
      "_count": {
        "messages": 12
      }
    }
  ]
}
Source: src/app/api/tpo/admin/dates/route.ts:8

GET /api/tpo/admin/messages

Fetches all messages for a specific date. Authentication: Internal API key
dateId
string
required
Date ID to fetch messages for
messages
object[]
Array of messages in chronological order

Request Example

curl "https://josh.example.com/api/tpo/admin/messages?dateId=clx789ghi" \
  -H "x-internal-api-key: your-secret-key"

Response Example

{
  "messages": [
    {
      "id": "clxmsg001",
      "createdAt": "2026-03-02T10:05:00Z",
      "fromPhone": "system",
      "toPhone": "+12125551234",
      "body": "let's get the date on the calendar. how does Friday, March 7th at 7pm work for you?",
      "blocked": false
    },
    {
      "id": "clxmsg002",
      "createdAt": "2026-03-02T10:07:00Z",
      "fromPhone": "+12125551234",
      "toPhone": "system",
      "body": "works for me!",
      "blocked": false
    },
    {
      "id": "clxmsg003",
      "createdAt": "2026-03-02T10:08:00Z",
      "fromPhone": "system",
      "toPhone": "+12125555678",
      "body": "your match is free on Friday, March 7th at 7pm. does that work for you?",
      "blocked": false
    }
  ]
}
Source: src/app/api/tpo/admin/messages/route.ts:8

POST /api/tpo/admin/end-date

Manually ends an active date. Authentication: Internal API key
dateId
string
required
Date ID to end
success
boolean
Whether date was ended

Request Example

curl -X POST https://josh.example.com/api/tpo/admin/end-date \
  -H "Content-Type: application/json" \
  -H "x-internal-api-key: your-secret-key" \
  -d '{"dateId": "clx789ghi"}'

Response Example

{
  "success": true
}
Source: src/app/api/tpo/admin/end-date/route.ts:10

POST /api/tpo/admin/signed-url

Generates temporary signed URLs for Supabase storage objects. Authentication: Internal API key
paths
string[]
required
Array of storage paths (e.g., ["photos/2125551234/1234567890.jpg"])
size
string
thumbnail (400x400) or full (1200px width). Defaults to thumbnail.
urls
object
Map of original paths to signed URLs

Request Example

curl -X POST https://josh.example.com/api/tpo/admin/signed-url \
  -H "Content-Type: application/json" \
  -H "x-internal-api-key: your-secret-key" \
  -d '{
    "paths": [
      "photos/2125551234/1234567890.jpg",
      "ids/2125551234/9876543210.jpg"
    ],
    "size": "full"
  }'

Response Example

{
  "urls": {
    "photos/2125551234/1234567890.jpg": "https://supabase.co/storage/v1/object/sign/tpo-uploads/photos/2125551234/1234567890.jpg?token=xyz&transform=...",
    "ids/2125551234/9876543210.jpg": "https://supabase.co/storage/v1/object/sign/tpo-uploads/ids/2125551234/9876543210.jpg?token=abc&transform=..."
  }
}
Source: src/app/api/tpo/admin/signed-url/route.ts:53
Signed URLs expire after 1 hour. For direct URLs (legacy fallback refs), the original URL is returned.

Error Responses

All endpoints return consistent error structures:
{
  "message": "userId and action (approve|reject) are required"
}

Authentication

Internal API Key

Admin endpoints require the x-internal-api-key header:
const key = req.headers.get("x-internal-api-key");
if (!hasValidInternalApiKey(key)) {
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
Set via environment variable:
INTERNAL_API_KEY=your-secret-key

Surge Webhook Validation

The /api/tpo/webhook endpoint validates Surge signatures:
const signatureHeader = req.headers.get("surge-signature");
if (!validateSurgeSignature(signatureHeader, rawBody)) {
  return NextResponse.json({ message: "Invalid signature" }, { status: 401 });
}
Set via environment variable:
SURGE_WEBHOOK_SECRET=your-webhook-secret
Skip validation in development:
SURGE_SKIP_WEBHOOK_VALIDATION=true

Rate Limiting

Currently no rate limiting is implemented. Consider adding rate limiting for production deployments.

Architecture Overview

System design and AI integration points

Database Schema

Prisma models and relationships

Build docs developers (and LLMs) love