POST /auth/login
Authenticate user and receive JWT token
Request Body
Parameter Type Description
email required string
password required string
Example Request
bash
curl -X POST https://api.probiddr.com/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "john.doe@example.com",
    "password": "SecurePassword123!"
  }'
javascript
const response = await fetch('https://api.probiddr.com/auth/login', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "email": "john.doe@example.com",
    "password": "SecurePassword123!"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/auth/login',
    json={
      "email": "john.doe@example.com",
      "password": "SecurePassword123!"
    }
)

data = response.json()
Responses
200 OK
Login successful
json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "rft_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user": {
    "id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "email": "john.doe@example.com",
    "user_type": "homeowner",
    "first_name": "John",
    "last_name": "Doe",
    "phone": "+1-555-0123",
    "profile_picture_url": "https://cdn.probiddr.com/avatars/usr_123.jpg",
    "created_at": "2025-01-15T10:30:00Z",
    "updated_at": "2025-01-15T14:20:00Z"
  }
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /auth/signup/homeowner
Register a new homeowner account. **Welcome bonus:** 10 tokens credited on signup
Request Body
Parameter Type Description
email required string
password required string
first_name required string
last_name required string
phone string
referral_code string Optional referral code (10 tokens for both parties)
Example Request
bash
curl -X POST https://api.probiddr.com/auth/signup/homeowner \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane.homeowner@example.com",
    "password": "SecurePassword123!",
    "first_name": "Jane",
    "last_name": "Smith",
    "phone": "+1-555-0123",
    "referral_code": "JOHN2025"
  }'
javascript
const response = await fetch('https://api.probiddr.com/auth/signup/homeowner', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "email": "jane.homeowner@example.com",
    "password": "SecurePassword123!",
    "first_name": "Jane",
    "last_name": "Smith",
    "phone": "+1-555-0123",
    "referral_code": "JOHN2025"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/auth/signup/homeowner',
    json={
      "email": "jane.homeowner@example.com",
      "password": "SecurePassword123!",
      "first_name": "Jane",
      "last_name": "Smith",
      "phone": "+1-555-0123",
      "referral_code": "JOHN2025"
    }
)

data = response.json()
Responses
201 Created
Account created successfully
json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "rft_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user": {
    "id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "email": "john.doe@example.com",
    "user_type": "homeowner",
    "first_name": "John",
    "last_name": "Doe",
    "phone": "+1-555-0123",
    "profile_picture_url": "https://cdn.probiddr.com/avatars/usr_123.jpg",
    "created_at": "2025-01-15T10:30:00Z",
    "updated_at": "2025-01-15T14:20:00Z"
  },
  "homeowner": {
    "id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "address_line1": "123 Main St",
    "address_line2": "Apt 4B",
    "city": "San Francisco",
    "state": "CA",
    "zip_code": "94103",
    "country": "USA",
    "property_type": "single_family",
    "created_at": "2025-01-15T10:30:00Z",
    "updated_at": "2025-01-15T14:20:00Z"
  },
  "tokens_awarded": 20
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /auth/signup/contractor
Register a new contractor account. **Welcome bonus:** 25 tokens credited on signup
Request Body
Parameter Type Description
email required string
password required string
first_name required string
last_name required string
phone string
company_name string
referral_code string Optional referral code (10 tokens for both parties)
Example Request
bash
curl -X POST https://api.probiddr.com/auth/signup/contractor \
  -H "Content-Type: application/json" \
  -d '{
    "email": "mike.contractor@example.com",
    "password": "SecurePassword123!",
    "first_name": "Mike",
    "last_name": "Builder",
    "phone": "+1-555-0456",
    "company_name": "Builder Construction LLC",
    "referral_code": "JOHN2025"
  }'
javascript
const response = await fetch('https://api.probiddr.com/auth/signup/contractor', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "email": "mike.contractor@example.com",
    "password": "SecurePassword123!",
    "first_name": "Mike",
    "last_name": "Builder",
    "phone": "+1-555-0456",
    "company_name": "Builder Construction LLC",
    "referral_code": "JOHN2025"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/auth/signup/contractor',
    json={
      "email": "mike.contractor@example.com",
      "password": "SecurePassword123!",
      "first_name": "Mike",
      "last_name": "Builder",
      "phone": "+1-555-0456",
      "company_name": "Builder Construction LLC",
      "referral_code": "JOHN2025"
    }
)

data = response.json()
Responses
201 Created
Account created successfully
json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "rft_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user": {
    "id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "email": "john.doe@example.com",
    "user_type": "homeowner",
    "first_name": "John",
    "last_name": "Doe",
    "phone": "+1-555-0123",
    "profile_picture_url": "https://cdn.probiddr.com/avatars/usr_123.jpg",
    "created_at": "2025-01-15T10:30:00Z",
    "updated_at": "2025-01-15T14:20:00Z"
  },
  "contractor": {
    "id": "ctr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "company_name": "Doe Construction LLC",
    "business_type": "llc",
    "license_number": "CA-123456",
    "license_state": "CA",
    "insurance_provider": "State Farm",
    "insurance_policy_number": "POL-123456789",
    "years_in_business": 15,
    "service_areas": [
      "San Francisco",
      "Oakland",
      "Berkeley"
    ],
    "specialties": [
      "Kitchen Remodeling",
      "Bathroom Renovation",
      "General Contracting"
    ],
    "bio": "Experienced contractor with 15 years in residential remodeling",
    "website": "https://doeconstruction.com",
    "verified": true,
    "created_at": "2025-01-15T10:30:00Z",
    "updated_at": "2025-01-15T14:20:00Z"
  },
  "tokens_awarded": 35
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /auth/logout
Invalidate current session and JWT token
Example Request
bash
curl -X POST https://api.probiddr.com/auth/logout \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/auth/logout', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/auth/logout',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Logout successful
json
{
  "status": 200,
  "message": "Operation completed successfully",
  "data": {}
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /auth/refresh
Use refresh token to obtain a new JWT
Request Body
Parameter Type Description
refresh_token required string
Example Request
bash
curl -X POST https://api.probiddr.com/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "rft_3ADMbXXNXxxF5VHbDyG1UbsvJmC"
  }'
javascript
const response = await fetch('https://api.probiddr.com/auth/refresh', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "refresh_token": "rft_3ADMbXXNXxxF5VHbDyG1UbsvJmC"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/auth/refresh',
    json={
      "refresh_token": "rft_3ADMbXXNXxxF5VHbDyG1UbsvJmC"
    }
)

data = response.json()
Responses
200 OK
Token refreshed successfully
json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "rft_3ADMbZZZXxxF5VHbDyG1UbsvJmD"
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /jobs
Retrieve a paginated list of jobs with optional filtering. **Access control:** - Homeowners: See only their own jobs (all statuses) - Contractors: See only open jobs + jobs they've bid on - Admin: See all jobs **Filtering:** - Filter by status, category, urgency, or search terms - Search matches title and description fields
Parameters
Name Type Description
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
search
Query
string Search query string
status
Query
string Filter by job status
category
Query
string Filter by job category
urgency
Query
string Filter by urgency level
homeowner_id
Query
string Filter by homeowner (admin only)
Example Request
bash
curl -X GET https://api.probiddr.com/jobs \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/jobs', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/jobs',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
List of jobs retrieved successfully
json
{}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /jobs
Create a new job in draft status. Homeowners only. **Initial status:** draft **Required fields for draft:** - title - description **Required fields to publish (draft β†’ open):** - All draft fields plus: category, urgency, address_line1, city, state Use `POST /jobs/{id}/publish` to publish the job after creation.
Request Body
Parameter Type Description
title required string
description required string
category string
budget_min number
budget_max number
urgency string
address_line1 string
address_line2 string
city string
state string
zip_code string
country string
desired_start_date string
desired_completion_date string
requirements string
Example Request
bash
curl -X POST https://api.probiddr.com/jobs \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Kitchen Remodel",
    "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
    "category": "Remodeling",
    "budget_min": 5000,
    "budget_max": 10000,
    "urgency": "standard",
    "address_line1": "123 Main St",
    "address_line2": "Apt 4B",
    "city": "San Francisco",
    "state": "CA",
    "zip_code": "94103",
    "country": "USA",
    "desired_start_date": "2025-02-01",
    "desired_completion_date": "2025-03-15",
    "requirements": "Must be licensed and insured"
  }'
javascript
const response = await fetch('https://api.probiddr.com/jobs', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "title": "Kitchen Remodel",
    "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
    "category": "Remodeling",
    "budget_min": 5000,
    "budget_max": 10000,
    "urgency": "standard",
    "address_line1": "123 Main St",
    "address_line2": "Apt 4B",
    "city": "San Francisco",
    "state": "CA",
    "zip_code": "94103",
    "country": "USA",
    "desired_start_date": "2025-02-01",
    "desired_completion_date": "2025-03-15",
    "requirements": "Must be licensed and insured"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/jobs',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "title": "Kitchen Remodel",
      "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
      "category": "Remodeling",
      "budget_min": 5000,
      "budget_max": 10000,
      "urgency": "standard",
      "address_line1": "123 Main St",
      "address_line2": "Apt 4B",
      "city": "San Francisco",
      "state": "CA",
      "zip_code": "94103",
      "country": "USA",
      "desired_start_date": "2025-02-01",
      "desired_completion_date": "2025-03-15",
      "requirements": "Must be licensed and insured"
    }
)

data = response.json()
Responses
201 Created
Job created successfully in draft status
json
{
  "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "title": "Kitchen Remodel",
  "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
  "status": "open",
  "category": "Remodeling",
  "budget_min": 5000,
  "budget_max": 10000,
  "urgency": "standard",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "desired_start_date": "2025-02-01",
  "desired_completion_date": "2025-03-15",
  "requirements": "Must be licensed and insured",
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /jobs/{id}
Retrieve detailed information about a specific job. **Access control:** - Homeowners: Can view only their own jobs - Contractors: Can view open jobs + jobs they've bid on - Admin: Can view all jobs
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
Example Request
bash
curl -X GET https://api.probiddr.com/jobs/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/jobs/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Job details retrieved successfully
json
{
  "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "title": "Kitchen Remodel",
  "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
  "status": "open",
  "category": "Remodeling",
  "budget_min": 5000,
  "budget_max": 10000,
  "urgency": "standard",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "desired_start_date": "2025-02-01",
  "desired_completion_date": "2025-03-15",
  "requirements": "Must be licensed and insured",
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
PUT /jobs/{id}
Update job details. Field editability depends on current job status. **Homeowner-only action.** **Editable fields by status:** | Status | Editable Fields | |--------|----------------| | draft | ALL fields | | open | title, description, budget_min, budget_max, urgency, desired_start_date, desired_completion_date, requirements | | awarded | desired_start_date, desired_completion_date, requirements | | in_progress | desired_completion_date, requirements | | in_review+ | NONE (read-only) | **Business rules (status: open):** - budget_min/budget_max: Maximum 10% decrease allowed - urgency: Cannot downgrade (urgent→priority→standard) if bids exist - category: Cannot change after publishing **Business rules (status: in_progress):** - desired_completion_date: Can adjust by ±30 days only
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
Request Body
Parameter Type Description
title string
description string
budget_min number
budget_max number
urgency string
desired_start_date string
desired_completion_date string
requirements string
Example Request
bash
curl -X PUT https://api.probiddr.com/jobs/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Updated Kitchen Remodel",
    "description": "Updated description with more details",
    "budget_min": 5000,
    "budget_max": 9500,
    "urgency": "priority",
    "desired_start_date": "2025-02-05",
    "desired_completion_date": "2025-03-20",
    "requirements": "Must be licensed, insured, and bonded"
  }'
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "title": "Updated Kitchen Remodel",
    "description": "Updated description with more details",
    "budget_min": 5000,
    "budget_max": 9500,
    "urgency": "priority",
    "desired_start_date": "2025-02-05",
    "desired_completion_date": "2025-03-20",
    "requirements": "Must be licensed, insured, and bonded"
  })
});

const data = await response.json();
python
import requests

response = requests.put(
    'https://api.probiddr.com/jobs/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "title": "Updated Kitchen Remodel",
      "description": "Updated description with more details",
      "budget_min": 5000,
      "budget_max": 9500,
      "urgency": "priority",
      "desired_start_date": "2025-02-05",
      "desired_completion_date": "2025-03-20",
      "requirements": "Must be licensed, insured, and bonded"
    }
)

data = response.json()
Responses
200 OK
Job updated successfully
json
{
  "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "title": "Kitchen Remodel",
  "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
  "status": "open",
  "category": "Remodeling",
  "budget_min": 5000,
  "budget_max": 10000,
  "urgency": "standard",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "desired_start_date": "2025-02-01",
  "desired_completion_date": "2025-03-15",
  "requirements": "Must be licensed and insured",
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
DELETE /jobs/{id}
Delete a job. Only allowed for draft jobs. **Homeowner-only action.** Jobs in any other status must be cancelled using `POST /jobs/{id}/cancel`.
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
Example Request
bash
curl -X DELETE https://api.probiddr.com/jobs/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.delete(
    'https://api.probiddr.com/jobs/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Job deleted successfully
json
{
  "status": 200,
  "message": "Operation completed successfully",
  "data": {}
}
400 Bad Request
Job cannot be deleted (not in draft status)
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /jobs/{id}/bids
Retrieve all bids submitted for a specific job. **Access control:** - Homeowners: Can view all bids for their own jobs - Contractors: Can view only their own bid for this job - Admin: Can view all bids
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
status
Query
string Filter by bid status
Example Request
bash
curl -X GET https://api.probiddr.com/jobs/{id}/bids \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}/bids', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/jobs/{id}/bids',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Bids retrieved successfully
json
{
  "data": [
    {
      "id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
      "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
      "contractor_id": "ctr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
      "status": "pending",
      "amount": 7500,
      "estimated_duration_days": 21,
      "message": "I have 15 years of experience in kitchen remodeling and can start next week.",
      "proposed_start_date": "2025-02-01",
      "materials_included": true,
      "payment_terms": "50% upfront, 50% on completion",
      "guaranteed_view": false,
      "hold_id": "hld_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
      "created_at": "2025-01-15T10:30:00Z",
      "updated_at": "2025-01-15T14:20:00Z",
      "submitted_at": "2025-01-15T11:00:00Z"
    }
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /jobs/{id}/publish
Publish a draft job, making it visible to contractors. **State transition:** draft β†’ open **Homeowner-only action.** **Token cost (deducted on publish):** - standard: 5 tokens - priority: 15 tokens - urgent: 30 tokens **Required fields to publish:** - title, description, category, urgency - address_line1, city, state **Validation:** - All required fields must be present - Sufficient token balance
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
Example Request
bash
curl -X POST https://api.probiddr.com/jobs/{id}/publish \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}/publish', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/jobs/{id}/publish',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Job published successfully
json
{
  "job": {
    "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
    "title": "Kitchen Remodel",
    "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
    "status": "open",
    "category": "Remodeling",
    "budget_min": 5000,
    "budget_max": 10000,
    "urgency": "standard",
    "address_line1": "123 Main St",
    "address_line2": "Apt 4B",
    "city": "San Francisco",
    "state": "CA",
    "zip_code": "94103",
    "country": "USA",
    "latitude": 37.7749,
    "longitude": -122.4194,
    "desired_start_date": "2025-02-01",
    "desired_completion_date": "2025-03-15",
    "requirements": "Must be licensed and insured",
    "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
    "created_at": "2025-01-15T10:30:00Z",
    "updated_at": "2025-01-15T14:20:00Z"
  },
  "tokens_charged": 5,
  "remaining_balance": 45
}
400 Bad Request
Validation error or insufficient tokens
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /jobs/{id}/award
Award the job to a specific bid. **State transition:** open β†’ awarded **Homeowner-only action.** **Side effects:** - Accepted bid status: pending/viewed/shortlisted β†’ accepted - All other bids for this job: * β†’ rejected - Job's awarded_bid_id field updated
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
Request Body
Parameter Type Description
bid_id required string ID of the bid to accept
Example Request
bash
curl -X POST https://api.probiddr.com/jobs/{id}/award \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18"
  }'
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}/award', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/jobs/{id}/award',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18"
    }
)

data = response.json()
Responses
200 OK
Job awarded successfully
json
{
  "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "title": "Kitchen Remodel",
  "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
  "status": "open",
  "category": "Remodeling",
  "budget_min": 5000,
  "budget_max": 10000,
  "urgency": "standard",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "desired_start_date": "2025-02-01",
  "desired_completion_date": "2025-03-15",
  "requirements": "Must be licensed and insured",
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Invalid transition or bid not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /jobs/{id}/checkin
Contractor checks in to indicate work has started. **State transition:** awarded β†’ in_progress **Contractor-only action** (must be awarded contractor).
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
Request Body
Parameter Type Description
notes string Optional notes about work commencement
Example Request
bash
curl -X POST https://api.probiddr.com/jobs/{id}/checkin \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "notes": "Starting work today. Materials delivered."
  }'
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}/checkin', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "notes": "Starting work today. Materials delivered."
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/jobs/{id}/checkin',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "notes": "Starting work today. Materials delivered."
    }
)

data = response.json()
Responses
200 OK
Work started successfully
json
{
  "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "title": "Kitchen Remodel",
  "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
  "status": "open",
  "category": "Remodeling",
  "budget_min": 5000,
  "budget_max": 10000,
  "urgency": "standard",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "desired_start_date": "2025-02-01",
  "desired_completion_date": "2025-03-15",
  "requirements": "Must be licensed and insured",
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Not the awarded contractor
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /jobs/{id}/submit-for-review
Contractor submits completed work for homeowner review. **State transition:** in_progress β†’ in_review **Contractor-only action** (must be awarded contractor).
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
Request Body
Parameter Type Description
completion_notes string Notes about completed work
Example Request
bash
curl -X POST https://api.probiddr.com/jobs/{id}/submit-for-review \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "completion_notes": "All work completed per specifications. Ready for inspection."
  }'
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}/submit-for-review', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "completion_notes": "All work completed per specifications. Ready for inspection."
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/jobs/{id}/submit-for-review',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "completion_notes": "All work completed per specifications. Ready for inspection."
    }
)

data = response.json()
Responses
200 OK
Work submitted for review successfully
json
{
  "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "title": "Kitchen Remodel",
  "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
  "status": "open",
  "category": "Remodeling",
  "budget_min": 5000,
  "budget_max": 10000,
  "urgency": "standard",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "desired_start_date": "2025-02-01",
  "desired_completion_date": "2025-03-15",
  "requirements": "Must be licensed and insured",
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /jobs/{id}/approve
Homeowner approves completed work. **State transition:** in_review β†’ completed **Homeowner-only action.** **Side effects:** - Job marked as successfully completed - Tokens released if any holds exist - Contractor credited for completion
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
Request Body
Parameter Type Description
rating integer Optional rating for contractor
review string Optional review text
Example Request
bash
curl -X POST https://api.probiddr.com/jobs/{id}/approve \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "rating": 5,
    "review": "Excellent work! Very professional."
  }'
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}/approve', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "rating": 5,
    "review": "Excellent work! Very professional."
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/jobs/{id}/approve',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "rating": 5,
      "review": "Excellent work! Very professional."
    }
)

data = response.json()
Responses
200 OK
Job approved and completed successfully
json
{
  "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "title": "Kitchen Remodel",
  "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
  "status": "open",
  "category": "Remodeling",
  "budget_min": 5000,
  "budget_max": 10000,
  "urgency": "standard",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "desired_start_date": "2025-02-01",
  "desired_completion_date": "2025-03-15",
  "requirements": "Must be licensed and insured",
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /jobs/{id}/dispute
Homeowner disputes the completed work quality or scope. **State transition:** in_review β†’ disputed **Homeowner-only action.** Initiates dispute resolution process between homeowner and contractor.
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
Request Body
Parameter Type Description
reason required string Detailed reason for dispute
details string Additional details or evidence
Example Request
bash
curl -X POST https://api.probiddr.com/jobs/{id}/dispute \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Work quality does not match agreed specifications",
    "details": "Cabinets are misaligned and paint finish is uneven"
  }'
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}/dispute', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "reason": "Work quality does not match agreed specifications",
    "details": "Cabinets are misaligned and paint finish is uneven"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/jobs/{id}/dispute',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "reason": "Work quality does not match agreed specifications",
      "details": "Cabinets are misaligned and paint finish is uneven"
    }
)

data = response.json()
Responses
200 OK
Dispute filed successfully
json
{
  "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "title": "Kitchen Remodel",
  "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
  "status": "open",
  "category": "Remodeling",
  "budget_min": 5000,
  "budget_max": 10000,
  "urgency": "standard",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "desired_start_date": "2025-02-01",
  "desired_completion_date": "2025-03-15",
  "requirements": "Must be licensed and insured",
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /jobs/{id}/escalate
Escalate unresolved dispute to admin review. **State transition:** disputed β†’ escalated **Available to:** Either homeowner or contractor involved in the job. Used when parties cannot resolve dispute through direct communication.
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
Request Body
Parameter Type Description
escalation_reason required string Reason for escalating to admin
Example Request
bash
curl -X POST https://api.probiddr.com/jobs/{id}/escalate \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "escalation_reason": "Unable to reach agreement on resolution"
  }'
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}/escalate', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "escalation_reason": "Unable to reach agreement on resolution"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/jobs/{id}/escalate',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "escalation_reason": "Unable to reach agreement on resolution"
    }
)

data = response.json()
Responses
200 OK
Dispute escalated successfully
json
{
  "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "title": "Kitchen Remodel",
  "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
  "status": "open",
  "category": "Remodeling",
  "budget_min": 5000,
  "budget_max": 10000,
  "urgency": "standard",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "desired_start_date": "2025-02-01",
  "desired_completion_date": "2025-03-15",
  "requirements": "Must be licensed and insured",
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /jobs/{id}/reach-decision
Admin makes a decision on the escalated dispute. **State transition:** escalated β†’ decision_reached **Admin-only action.**
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
Request Body
Parameter Type Description
decision required string Admin's ruling
ruling required string Detailed explanation of decision
resolution_terms string Terms of resolution if applicable
Example Request
bash
curl -X POST https://api.probiddr.com/jobs/{id}/reach-decision \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "decision": "favor_contractor",
    "ruling": "Work meets contract specifications. Minor issues do not warrant rejection.",
    "resolution_terms": "Contractor to fix cabinet alignment within 48 hours"
  }'
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}/reach-decision', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "decision": "favor_contractor",
    "ruling": "Work meets contract specifications. Minor issues do not warrant rejection.",
    "resolution_terms": "Contractor to fix cabinet alignment within 48 hours"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/jobs/{id}/reach-decision',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "decision": "favor_contractor",
      "ruling": "Work meets contract specifications. Minor issues do not warrant rejection.",
      "resolution_terms": "Contractor to fix cabinet alignment within 48 hours"
    }
)

data = response.json()
Responses
200 OK
Decision recorded successfully
json
{
  "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "title": "Kitchen Remodel",
  "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
  "status": "open",
  "category": "Remodeling",
  "budget_min": 5000,
  "budget_max": 10000,
  "urgency": "standard",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "desired_start_date": "2025-02-01",
  "desired_completion_date": "2025-03-15",
  "requirements": "Must be licensed and insured",
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /jobs/{id}/file-appeal
Contractor files appeal against admin decision. **State transition:** decision_reached β†’ appealed **Contractor-only action** (must be awarded contractor). Final escalation step before admin makes final ruling.
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
Request Body
Parameter Type Description
appeal_reason required string Reason for appealing the decision
supporting_evidence string Additional evidence or arguments
Example Request
bash
curl -X POST https://api.probiddr.com/jobs/{id}/file-appeal \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "appeal_reason": "Decision did not consider all evidence provided",
    "supporting_evidence": "Attached photos showing work meets specifications"
  }'
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}/file-appeal', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "appeal_reason": "Decision did not consider all evidence provided",
    "supporting_evidence": "Attached photos showing work meets specifications"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/jobs/{id}/file-appeal',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "appeal_reason": "Decision did not consider all evidence provided",
      "supporting_evidence": "Attached photos showing work meets specifications"
    }
)

data = response.json()
Responses
200 OK
Appeal filed successfully
json
{
  "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "title": "Kitchen Remodel",
  "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
  "status": "open",
  "category": "Remodeling",
  "budget_min": 5000,
  "budget_max": 10000,
  "urgency": "standard",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "desired_start_date": "2025-02-01",
  "desired_completion_date": "2025-03-15",
  "requirements": "Must be licensed and insured",
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /jobs/{id}/finalize-appeal
Admin makes final decision on appeal. **State transition:** appealed β†’ completed **Admin-only action.** This is the terminal decision - no further appeals allowed.
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
Request Body
Parameter Type Description
final_decision required string Final ruling on appeal
ruling required string Final explanation
Example Request
bash
curl -X POST https://api.probiddr.com/jobs/{id}/finalize-appeal \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "final_decision": "uphold_original",
    "ruling": "Original decision stands. All evidence reviewed."
  }'
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}/finalize-appeal', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "final_decision": "uphold_original",
    "ruling": "Original decision stands. All evidence reviewed."
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/jobs/{id}/finalize-appeal',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "final_decision": "uphold_original",
      "ruling": "Original decision stands. All evidence reviewed."
    }
)

data = response.json()
Responses
200 OK
Appeal finalized successfully
json
{
  "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "title": "Kitchen Remodel",
  "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
  "status": "open",
  "category": "Remodeling",
  "budget_min": 5000,
  "budget_max": 10000,
  "urgency": "standard",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "desired_start_date": "2025-02-01",
  "desired_completion_date": "2025-03-15",
  "requirements": "Must be licensed and insured",
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /jobs/{id}/resolve-dispute
Resolve dispute through agreement between parties. **State transition:** disputed β†’ completed **Available to:** Either party can propose, both must agree. Used when homeowner and contractor reach resolution without admin intervention.
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
Request Body
Parameter Type Description
resolution required string Terms of resolution agreement
agreed_by string Who is agreeing to this resolution
Example Request
bash
curl -X POST https://api.probiddr.com/jobs/{id}/resolve-dispute \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "resolution": "Contractor will fix identified issues within 24 hours",
    "agreed_by": "both"
  }'
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}/resolve-dispute', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "resolution": "Contractor will fix identified issues within 24 hours",
    "agreed_by": "both"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/jobs/{id}/resolve-dispute',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "resolution": "Contractor will fix identified issues within 24 hours",
      "agreed_by": "both"
    }
)

data = response.json()
Responses
200 OK
Dispute resolved successfully
json
{
  "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "title": "Kitchen Remodel",
  "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
  "status": "open",
  "category": "Remodeling",
  "budget_min": 5000,
  "budget_max": 10000,
  "urgency": "standard",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "desired_start_date": "2025-02-01",
  "desired_completion_date": "2025-03-15",
  "requirements": "Must be licensed and insured",
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /jobs/{id}/complete
Force complete a job from any status. **State transition:** any β†’ completed **Admin-only action.** Used for administrative cleanup or special circumstances. Bypasses normal state machine.
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
Request Body
Parameter Type Description
reason required string Admin reason for force completion
Example Request
bash
curl -X POST https://api.probiddr.com/jobs/{id}/complete \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Customer service request - parties resolved offline"
  }'
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}/complete', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "reason": "Customer service request - parties resolved offline"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/jobs/{id}/complete',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "reason": "Customer service request - parties resolved offline"
    }
)

data = response.json()
Responses
200 OK
Job completed successfully
json
{
  "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "title": "Kitchen Remodel",
  "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
  "status": "open",
  "category": "Remodeling",
  "budget_min": 5000,
  "budget_max": 10000,
  "urgency": "standard",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "desired_start_date": "2025-02-01",
  "desired_completion_date": "2025-03-15",
  "requirements": "Must be licensed and insured",
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /jobs/{id}/cancel
Cancel a job from any status. **State transition:** any β†’ cancelled **Homeowner-only action.** **Token refund policy:** - draft/open: Full refund of posting tokens - awarded+: No refund (job already in progress or assigned) **Side effects:** - All pending bids: * β†’ withdrawn (with token refund for pending bids) - Token holds released
Parameters
Name Type Description
id required
Path
string Job KSUID identifier
Request Body
Parameter Type Description
reason string Optional reason for cancellation
Example Request
bash
curl -X POST https://api.probiddr.com/jobs/{id}/cancel \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Project postponed indefinitely"
  }'
javascript
const response = await fetch('https://api.probiddr.com/jobs/{id}/cancel', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "reason": "Project postponed indefinitely"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/jobs/{id}/cancel',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "reason": "Project postponed indefinitely"
    }
)

data = response.json()
Responses
200 OK
Job cancelled successfully
json
{
  "job": {
    "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
    "title": "Kitchen Remodel",
    "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
    "status": "open",
    "category": "Remodeling",
    "budget_min": 5000,
    "budget_max": 10000,
    "urgency": "standard",
    "address_line1": "123 Main St",
    "address_line2": "Apt 4B",
    "city": "San Francisco",
    "state": "CA",
    "zip_code": "94103",
    "country": "USA",
    "latitude": 37.7749,
    "longitude": -122.4194,
    "desired_start_date": "2025-02-01",
    "desired_completion_date": "2025-03-15",
    "requirements": "Must be licensed and insured",
    "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
    "created_at": "2025-01-15T10:30:00Z",
    "updated_at": "2025-01-15T14:20:00Z"
  },
  "tokens_refunded": 5
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /homeowners
Retrieve a paginated list of homeowners with optional filtering. **Access control:** - Admin-only endpoint - Returns all homeowner profiles **Filtering:** - Search matches user's first/last name fields
Parameters
Name Type Description
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
search
Query
string Search query string
Example Request
bash
curl -X GET https://api.probiddr.com/homeowners \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/homeowners', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/homeowners',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
List of homeowners retrieved successfully
json
{}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /homeowners
Create a new homeowner profile for the authenticated user. **User must be authenticated.** **Business rules:** - User can only create one homeowner profile - Phone number is required for job notifications - Address information is optional during profile creation - Address can be specified per-job basis **Welcome bonus:** - New homeowners receive 10 tokens upon profile creation
Request Body
Parameter Type Description
user_id required string User KSUID
phone_number required string Phone number for notifications
address_line1 string
address_line2 string
city string
state string
zip_code string
country string
Example Request
bash
curl -X POST https://api.probiddr.com/homeowners \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "phone_number": "+14155551234",
    "address_line1": "123 Main St",
    "address_line2": "Apt 4B",
    "city": "San Francisco",
    "state": "CA",
    "zip_code": "94103",
    "country": "USA"
  }'
javascript
const response = await fetch('https://api.probiddr.com/homeowners', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "phone_number": "+14155551234",
    "address_line1": "123 Main St",
    "address_line2": "Apt 4B",
    "city": "San Francisco",
    "state": "CA",
    "zip_code": "94103",
    "country": "USA"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/homeowners',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
      "phone_number": "+14155551234",
      "address_line1": "123 Main St",
      "address_line2": "Apt 4B",
      "city": "San Francisco",
      "state": "CA",
      "zip_code": "94103",
      "country": "USA"
    }
)

data = response.json()
Responses
201 Created
Homeowner profile created successfully
json
{
  "homeowner": {
    "id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "address_line1": "123 Main St",
    "address_line2": "Apt 4B",
    "city": "San Francisco",
    "state": "CA",
    "zip_code": "94103",
    "country": "USA",
    "property_type": "single_family",
    "created_at": "2025-01-15T10:30:00Z",
    "updated_at": "2025-01-15T14:20:00Z"
  },
  "welcome_bonus": 10
}
400 Bad Request
Validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /homeowner/user/{user_id}
Retrieve homeowner profile by associated user_id. **Access control:** - Homeowners: Can view only their own profile - Admin: Can view all profiles **Use case:** - Useful when you have the user_id from authentication but need homeowner_id for job creation
Parameters
Name Type Description
user_id required
Path
string User KSUID identifier
Example Request
bash
curl -X GET https://api.probiddr.com/homeowner/user/{user_id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/homeowner/user/{user_id}', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/homeowner/user/{user_id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Homeowner profile retrieved successfully
json
{
  "id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "property_type": "single_family",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
No homeowner profile found for this user
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /homeowners/{id}
Retrieve detailed information about a specific homeowner. **Access control:** - Homeowners: Can view only their own profile - Contractors: Can view profiles of homeowners with jobs they've bid on - Admin: Can view all profiles **Privacy:** - Full address only visible to profile owner and admin - Contractors see city/state only
Parameters
Name Type Description
id required
Path
string Homeowner KSUID identifier
Example Request
bash
curl -X GET https://api.probiddr.com/homeowners/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/homeowners/{id}', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/homeowners/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Homeowner profile retrieved successfully
json
{
  "id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "property_type": "single_family",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
PUT /homeowners/{id}
Update homeowner profile details. **Homeowner-only action** (must own this profile). **Non-editable fields:** - user_id
Parameters
Name Type Description
id required
Path
string Homeowner KSUID identifier
Request Body
Parameter Type Description
phone_number string
address_line1 string
address_line2 string
city string
state string
zip_code string
country string
Example Request
bash
curl -X PUT https://api.probiddr.com/homeowners/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+14155559876",
    "address_line1": "456 Oak Avenue",
    "address_line2": "Suite 200",
    "city": "Oakland",
    "state": "CA",
    "zip_code": "94612",
    "country": "USA"
  }'
javascript
const response = await fetch('https://api.probiddr.com/homeowners/{id}', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "phone_number": "+14155559876",
    "address_line1": "456 Oak Avenue",
    "address_line2": "Suite 200",
    "city": "Oakland",
    "state": "CA",
    "zip_code": "94612",
    "country": "USA"
  })
});

const data = await response.json();
python
import requests

response = requests.put(
    'https://api.probiddr.com/homeowners/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "phone_number": "+14155559876",
      "address_line1": "456 Oak Avenue",
      "address_line2": "Suite 200",
      "city": "Oakland",
      "state": "CA",
      "zip_code": "94612",
      "country": "USA"
    }
)

data = response.json()
Responses
200 OK
Homeowner profile updated successfully
json
{
  "id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94103",
  "country": "USA",
  "property_type": "single_family",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
DELETE /homeowners/{id}
Delete a homeowner profile. **Homeowner-only action** (must own this profile). **Business rules:** - Cannot delete if there are active jobs (open, awarded, in_progress, in_review, disputed) - Historical data (completed jobs, reviews) will be anonymized - Token balance must be zero or will be forfeited
Parameters
Name Type Description
id required
Path
string Homeowner KSUID identifier
Example Request
bash
curl -X DELETE https://api.probiddr.com/homeowners/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/homeowners/{id}', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.delete(
    'https://api.probiddr.com/homeowners/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Homeowner profile deleted successfully
json
{
  "status": 200,
  "message": "Homeowner profile deleted successfully",
  "tokens_forfeited": 0
}
400 Bad Request
Cannot delete due to active jobs or token balance
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /homeowners/{id}/jobs
Retrieve all jobs posted by a specific homeowner. **Access control:** - Homeowners: Can view only their own jobs (all statuses) - Contractors: Can view only open jobs from this homeowner - Admin: Can view all jobs **Filtering:** - Filter by status
Parameters
Name Type Description
id required
Path
string Homeowner KSUID identifier
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
status
Query
string Filter by job status
Example Request
bash
curl -X GET https://api.probiddr.com/homeowners/{id}/jobs \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/homeowners/{id}/jobs', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/homeowners/{id}/jobs',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Homeowner jobs retrieved successfully
json
{}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /homeowners/{id}/reviews
⚠️ Coming Soon - Retrieve reviews left by or for a specific homeowner. **This endpoint is a stub and not yet implemented.** **Planned features:** - Reviews left by homeowner for contractors - Reviews left by contractors for homeowner (professionalism, clarity) - Filter by rating value - Sort by date or helpfulness
Parameters
Name Type Description
id required
Path
string Homeowner KSUID identifier
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
Example Request
bash
curl -X GET https://api.probiddr.com/homeowners/{id}/reviews \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/homeowners/{id}/reviews', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/homeowners/{id}/reviews',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
501
Not implemented yet
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /homeowners/{id}/stats
Retrieve statistics for a specific homeowner. **Access control:** - Homeowners: Can view only their own stats - Admin: Can view all stats **Statistics include:** - Total jobs posted by status - Total amount spent on completed jobs - Average job value - Total bids received across all jobs
Parameters
Name Type Description
id required
Path
string Homeowner KSUID identifier
Example Request
bash
curl -X GET https://api.probiddr.com/homeowners/{id}/stats \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/homeowners/{id}/stats', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/homeowners/{id}/stats',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Homeowner statistics retrieved successfully
json
{
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "total_jobs_posted": 12,
  "active_jobs": 2,
  "completed_jobs": 8,
  "total_spent": 45000,
  "average_rating": 4.5,
  "total_reviews": 8
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /contractors
Retrieve a paginated list of contractors with optional filtering. **Access control:** - Public endpoint (no authentication required) - Returns verified contractors only by default - Admin: Can view all contractors including unverified **Filtering:** - Filter by service area, specialty, or verification status - Search matches company name and bio fields
Parameters
Name Type Description
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
search
Query
string Search query string
service_area
Query
string Filter by service area (city or region)
specialty
Query
string Filter by specialty/category
verified
Query
boolean Filter by verification status (admin only)
Example Request
bash
curl -X GET https://api.probiddr.com/contractors \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/contractors', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/contractors',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
List of contractors retrieved successfully
json
{}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /contractors
Create a new contractor profile for the authenticated user. **User must be authenticated.** **Business rules:** - User can only create one contractor profile - Profile starts with verified: false - Admin verification required before appearing in public listings **Recommended fields for completeness:** - company_name, business_type, license_number, insurance details - service_areas, specialties, bio
Request Body
Parameter Type Description
user_id required string User KSUID
company_name string
business_type string
license_number string
license_state string
insurance_provider string
insurance_policy_number string
years_in_business integer
service_areas array
specialties array
bio string
website string
Example Request
bash
curl -X POST https://api.probiddr.com/contractors \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "company_name": "Doe Construction LLC",
    "business_type": "llc",
    "license_number": "CA-123456",
    "license_state": "CA",
    "insurance_provider": "State Farm",
    "insurance_policy_number": "POL-123456789",
    "years_in_business": 15,
    "service_areas": [
      "San Francisco",
      "Oakland",
      "Berkeley"
    ],
    "specialties": [
      "Kitchen Remodeling",
      "Bathroom Renovation"
    ],
    "bio": "Experienced contractor with 15 years in residential remodeling",
    "website": "https://doeconstruction.com"
  }'
javascript
const response = await fetch('https://api.probiddr.com/contractors', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "company_name": "Doe Construction LLC",
    "business_type": "llc",
    "license_number": "CA-123456",
    "license_state": "CA",
    "insurance_provider": "State Farm",
    "insurance_policy_number": "POL-123456789",
    "years_in_business": 15,
    "service_areas": [
      "San Francisco",
      "Oakland",
      "Berkeley"
    ],
    "specialties": [
      "Kitchen Remodeling",
      "Bathroom Renovation"
    ],
    "bio": "Experienced contractor with 15 years in residential remodeling",
    "website": "https://doeconstruction.com"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/contractors',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
      "company_name": "Doe Construction LLC",
      "business_type": "llc",
      "license_number": "CA-123456",
      "license_state": "CA",
      "insurance_provider": "State Farm",
      "insurance_policy_number": "POL-123456789",
      "years_in_business": 15,
      "service_areas": [
        "San Francisco",
        "Oakland",
        "Berkeley"
      ],
      "specialties": [
        "Kitchen Remodeling",
        "Bathroom Renovation"
      ],
      "bio": "Experienced contractor with 15 years in residential remodeling",
      "website": "https://doeconstruction.com"
    }
)

data = response.json()
Responses
201 Created
Contractor profile created successfully
json
{
  "id": "ctr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "company_name": "Doe Construction LLC",
  "business_type": "llc",
  "license_number": "CA-123456",
  "license_state": "CA",
  "insurance_provider": "State Farm",
  "insurance_policy_number": "POL-123456789",
  "years_in_business": 15,
  "service_areas": [
    "San Francisco",
    "Oakland",
    "Berkeley"
  ],
  "specialties": [
    "Kitchen Remodeling",
    "Bathroom Renovation",
    "General Contracting"
  ],
  "bio": "Experienced contractor with 15 years in residential remodeling",
  "website": "https://doeconstruction.com",
  "verified": true,
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /contractors/{id}
Retrieve detailed information about a specific contractor. **Access control:** - Public endpoint (no authentication required) - Unverified profiles only visible to owner and admin
Parameters
Name Type Description
id required
Path
string Contractor KSUID identifier
Example Request
bash
curl -X GET https://api.probiddr.com/contractors/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/contractors/{id}', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/contractors/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Contractor profile retrieved successfully
json
{
  "id": "ctr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "company_name": "Doe Construction LLC",
  "business_type": "llc",
  "license_number": "CA-123456",
  "license_state": "CA",
  "insurance_provider": "State Farm",
  "insurance_policy_number": "POL-123456789",
  "years_in_business": 15,
  "service_areas": [
    "San Francisco",
    "Oakland",
    "Berkeley"
  ],
  "specialties": [
    "Kitchen Remodeling",
    "Bathroom Renovation",
    "General Contracting"
  ],
  "bio": "Experienced contractor with 15 years in residential remodeling",
  "website": "https://doeconstruction.com",
  "verified": true,
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
403 Forbidden
Profile is not verified and user is not authorized to view
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
PUT /contractors/{id}
Update contractor profile details. **Contractor-only action** (must own this profile). **Non-editable fields:** - user_id, verified (admin-only) **Note:** Changes to license/insurance information may reset verification status.
Parameters
Name Type Description
id required
Path
string Contractor KSUID identifier
Request Body
Parameter Type Description
company_name string
business_type string
license_number string
license_state string
insurance_provider string
insurance_policy_number string
years_in_business integer
service_areas array
specialties array
bio string
website string
Example Request
bash
curl -X PUT https://api.probiddr.com/contractors/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "company_name": "Doe Construction LLC",
    "business_type": "llc",
    "license_number": "CA-123456",
    "license_state": "CA",
    "insurance_provider": "State Farm",
    "insurance_policy_number": "POL-123456789",
    "years_in_business": 16,
    "service_areas": [
      "San Francisco",
      "Oakland",
      "Berkeley",
      "San Jose"
    ],
    "specialties": [
      "Kitchen Remodeling",
      "Bathroom Renovation",
      "General Contracting"
    ],
    "bio": "Updated professional biography",
    "website": "https://doeconstruction.com"
  }'
javascript
const response = await fetch('https://api.probiddr.com/contractors/{id}', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "company_name": "Doe Construction LLC",
    "business_type": "llc",
    "license_number": "CA-123456",
    "license_state": "CA",
    "insurance_provider": "State Farm",
    "insurance_policy_number": "POL-123456789",
    "years_in_business": 16,
    "service_areas": [
      "San Francisco",
      "Oakland",
      "Berkeley",
      "San Jose"
    ],
    "specialties": [
      "Kitchen Remodeling",
      "Bathroom Renovation",
      "General Contracting"
    ],
    "bio": "Updated professional biography",
    "website": "https://doeconstruction.com"
  })
});

const data = await response.json();
python
import requests

response = requests.put(
    'https://api.probiddr.com/contractors/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "company_name": "Doe Construction LLC",
      "business_type": "llc",
      "license_number": "CA-123456",
      "license_state": "CA",
      "insurance_provider": "State Farm",
      "insurance_policy_number": "POL-123456789",
      "years_in_business": 16,
      "service_areas": [
        "San Francisco",
        "Oakland",
        "Berkeley",
        "San Jose"
      ],
      "specialties": [
        "Kitchen Remodeling",
        "Bathroom Renovation",
        "General Contracting"
      ],
      "bio": "Updated professional biography",
      "website": "https://doeconstruction.com"
    }
)

data = response.json()
Responses
200 OK
Contractor profile updated successfully
json
{
  "id": "ctr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "company_name": "Doe Construction LLC",
  "business_type": "llc",
  "license_number": "CA-123456",
  "license_state": "CA",
  "insurance_provider": "State Farm",
  "insurance_policy_number": "POL-123456789",
  "years_in_business": 15,
  "service_areas": [
    "San Francisco",
    "Oakland",
    "Berkeley"
  ],
  "specialties": [
    "Kitchen Remodeling",
    "Bathroom Renovation",
    "General Contracting"
  ],
  "bio": "Experienced contractor with 15 years in residential remodeling",
  "website": "https://doeconstruction.com",
  "verified": true,
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
DELETE /contractors/{id}
Delete a contractor profile. **Contractor-only action** (must own this profile). **Business rules:** - Cannot delete if there are active bids (pending/accepted) - Cannot delete if involved in active jobs (awarded, in_progress, in_review, disputed) - Historical data (completed jobs, reviews) will be anonymized
Parameters
Name Type Description
id required
Path
string Contractor KSUID identifier
Example Request
bash
curl -X DELETE https://api.probiddr.com/contractors/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/contractors/{id}', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.delete(
    'https://api.probiddr.com/contractors/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Contractor profile deleted successfully
json
{
  "status": 200,
  "message": "Operation completed successfully",
  "data": {}
}
400 Bad Request
Cannot delete due to active bids or jobs
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /contractors/{id}/bids
Retrieve all bids submitted by a specific contractor. **Access control:** - Contractors: Can view only their own bids (all statuses) - Homeowners: Can view bids from contractors on their jobs (excluding draft) - Admin: Can view all bids **Filtering:** - Filter by status or job_id
Parameters
Name Type Description
id required
Path
string Contractor KSUID identifier
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
status
Query
string Filter by bid status
job_id
Query
string Filter by job ID
Example Request
bash
curl -X GET https://api.probiddr.com/contractors/{id}/bids \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/contractors/{id}/bids', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/contractors/{id}/bids',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Contractor bids retrieved successfully
json
{}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /contractors/{id}/reviews
⚠️ Coming Soon - Retrieve reviews for a specific contractor. **This endpoint is a stub and not yet implemented.** **Planned features:** - List all reviews for contractor - Include ratings, comments, and job details - Filter by rating value - Sort by date or helpfulness
Parameters
Name Type Description
id required
Path
string Contractor KSUID identifier
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
Example Request
bash
curl -X GET https://api.probiddr.com/contractors/{id}/reviews \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/contractors/{id}/reviews', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/contractors/{id}/reviews',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
501
Not implemented yet
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /contractors/{id}/stats
Retrieve statistics for a specific contractor. **Access control:** - Public endpoint (no authentication required) - Only available for verified contractors **Statistics include:** - Total jobs completed - Total bids submitted (pending, accepted, rejected) - Average rating (when reviews feature is implemented) - Success rate (bids accepted / total bids)
Parameters
Name Type Description
id required
Path
string Contractor KSUID identifier
Example Request
bash
curl -X GET https://api.probiddr.com/contractors/{id}/stats \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/contractors/{id}/stats', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/contractors/{id}/stats',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Contractor statistics retrieved successfully
json
{
  "contractor_id": "ctr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "jobs_completed": 47,
  "bids_pending": 5,
  "bids_accepted": 52,
  "bids_rejected": 89,
  "success_rate": 36.88,
  "average_rating": null
}
403 Forbidden
Profile is not verified
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /bids
Retrieve a paginated list of bids with optional filtering. **Access control:** - Contractors: See only their own bids (all statuses including draft) - Homeowners: See bids on their jobs (excluding draft and withdrawn) - Admin: See all bids **Withdrawn bid visibility:** - Homeowners see withdrawn bids as metadata only (no pricing/proposal details) - Contractors see full details of their own withdrawn bids **Filtering:** - Filter by status, job_id, or contractor_id - Draft bids only visible when filtering by contractor_id
Parameters
Name Type Description
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
status
Query
string Filter by bid status
job_id
Query
string Filter by job ID
contractor_id
Query
string Filter by contractor ID
Example Request
bash
curl -X GET https://api.probiddr.com/bids \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/bids', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/bids',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
List of bids retrieved successfully
json
{}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /bids
Create a new bid in draft status. Contractors only. **Initial status:** draft **Required fields for draft:** - job_id - contractor_id - amount - message **Optional fields:** - estimated_duration_days - proposed_start_date - materials_included - payment_terms Use `POST /bids/{id}/submit` to submit the bid after creation. **Business rules:** - Job must be in 'open' status - Contractor can only have one active bid per job (draft or pending+) - Cannot bid on your own job (if you're also a homeowner)
Request Body
Parameter Type Description
job_id required string ID of the job to bid on
contractor_id required string Contractor KSUID
amount required number Bid amount in USD
estimated_duration_days integer Estimated project duration in days
message required string Contractor's message to homeowner
proposed_start_date string Contractor's proposed start date
materials_included boolean Whether materials are included in the bid amount
payment_terms string Payment terms and schedule
Example Request
bash
curl -X POST https://api.probiddr.com/bids \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
    "contractor_id": "ctr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "amount": 7500,
    "estimated_duration_days": 21,
    "message": "I have 15 years of experience in kitchen remodeling and can start next week.",
    "proposed_start_date": "2025-02-01",
    "materials_included": true,
    "payment_terms": "50% upfront, 50% on completion"
  }'
javascript
const response = await fetch('https://api.probiddr.com/bids', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
    "contractor_id": "ctr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "amount": 7500,
    "estimated_duration_days": 21,
    "message": "I have 15 years of experience in kitchen remodeling and can start next week.",
    "proposed_start_date": "2025-02-01",
    "materials_included": true,
    "payment_terms": "50% upfront, 50% on completion"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/bids',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
      "contractor_id": "ctr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
      "amount": 7500,
      "estimated_duration_days": 21,
      "message": "I have 15 years of experience in kitchen remodeling and can start next week.",
      "proposed_start_date": "2025-02-01",
      "materials_included": true,
      "payment_terms": "50% upfront, 50% on completion"
    }
)

data = response.json()
Responses
201 Created
Bid created successfully in draft status
json
{
  "id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "contractor_id": "ctr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "status": "pending",
  "amount": 7500,
  "estimated_duration_days": 21,
  "message": "I have 15 years of experience in kitchen remodeling and can start next week.",
  "proposed_start_date": "2025-02-01",
  "materials_included": true,
  "payment_terms": "50% upfront, 50% on completion",
  "guaranteed_view": false,
  "hold_id": "hld_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z",
  "submitted_at": "2025-01-15T11:00:00Z"
}
400 Bad Request
Validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Job not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /bids/{id}
Retrieve detailed information about a specific bid. **Access control:** - Contractors: Can view only their own bids (all statuses including draft) - Homeowners: Can view bids on their jobs (excluding draft, withdrawn shows metadata only) - Admin: Can view all bids **Side effects:** - Emits `bid.viewed` event (when homeowner views a submitted bid) - May trigger automatic state transition: pending β†’ viewed (system-driven) **Withdrawn bids:** - Homeowners receive 403 error when trying to view withdrawn bid details - Contractors can view their own withdrawn bids
Parameters
Name Type Description
id required
Path
string Bid KSUID identifier
Example Request
bash
curl -X GET https://api.probiddr.com/bids/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/bids/{id}', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/bids/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Bid details retrieved successfully
json
{
  "id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "contractor_id": "ctr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "status": "pending",
  "amount": 7500,
  "estimated_duration_days": 21,
  "message": "I have 15 years of experience in kitchen remodeling and can start next week.",
  "proposed_start_date": "2025-02-01",
  "materials_included": true,
  "payment_terms": "50% upfront, 50% on completion",
  "guaranteed_view": false,
  "hold_id": "hld_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z",
  "submitted_at": "2025-01-15T11:00:00Z"
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Forbidden - cannot view withdrawn bid details or draft bid
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
PUT /bids/{id}
Update bid details. Field editability depends on current bid status. **Contractor-only action.** **CRITICAL: Bids are immutable after submission.** **Editable fields by status:** | Status | Editable Fields | |--------|----------------| | draft | ALL fields | | pending+ | NONE (read-only) | Once a bid is submitted (status changes from draft to pending), all fields become immutable. Contractors must withdraw and create a new bid to make changes. **Business rules:** - Only the contractor who created the bid can update it - Cannot change job_id or contractor_id - Cannot change guaranteed_view after submission
Parameters
Name Type Description
id required
Path
string Bid KSUID identifier
Request Body
Parameter Type Description
amount number
estimated_duration_days integer
message string
proposed_start_date string
materials_included boolean
payment_terms string
Example Request
bash
curl -X PUT https://api.probiddr.com/bids/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 8000,
    "estimated_duration_days": 18,
    "message": "Updated proposal with faster timeline",
    "proposed_start_date": "2025-01-25",
    "materials_included": true,
    "payment_terms": "30% upfront, 50% mid-project, 20% on completion"
  }'
javascript
const response = await fetch('https://api.probiddr.com/bids/{id}', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "amount": 8000,
    "estimated_duration_days": 18,
    "message": "Updated proposal with faster timeline",
    "proposed_start_date": "2025-01-25",
    "materials_included": true,
    "payment_terms": "30% upfront, 50% mid-project, 20% on completion"
  })
});

const data = await response.json();
python
import requests

response = requests.put(
    'https://api.probiddr.com/bids/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "amount": 8000,
      "estimated_duration_days": 18,
      "message": "Updated proposal with faster timeline",
      "proposed_start_date": "2025-01-25",
      "materials_included": true,
      "payment_terms": "30% upfront, 50% mid-project, 20% on completion"
    }
)

data = response.json()
Responses
200 OK
Bid updated successfully
json
{
  "id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "contractor_id": "ctr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "status": "pending",
  "amount": 7500,
  "estimated_duration_days": 21,
  "message": "I have 15 years of experience in kitchen remodeling and can start next week.",
  "proposed_start_date": "2025-02-01",
  "materials_included": true,
  "payment_terms": "50% upfront, 50% on completion",
  "guaranteed_view": false,
  "hold_id": "hld_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z",
  "submitted_at": "2025-01-15T11:00:00Z"
}
400 Bad Request
Validation error or bid is immutable
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
DELETE /bids/{id}
Withdraw a bid. Only allowed for draft and pending bids. **Contractor-only action.** **State transition:** draft/pending β†’ withdrawn **Token refund policy:** - draft: No refund needed (no tokens charged yet) - pending: Full refund of submission tokens (5/15/25 based on job urgency) - viewed/shortlisted: No refund, no withdrawal allowed (bid already seen by homeowner) **Guaranteed view:** - If bid used guaranteed_view feature, the hold is released (no charge) **Business rules:** - Cannot withdraw accepted bids (use job dispute process instead) - Cannot withdraw after homeowner has viewed (viewed/shortlisted status) - Once withdrawn, bid cannot be un-withdrawn (terminal state)
Parameters
Name Type Description
id required
Path
string Bid KSUID identifier
Request Body
Parameter Type Description
reason string Optional reason for withdrawal
Example Request
bash
curl -X DELETE https://api.probiddr.com/bids/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -d '{
    "reason": "Found a conflict in my schedule"
  }'
javascript
const response = await fetch('https://api.probiddr.com/bids/{id}', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "reason": "Found a conflict in my schedule"
  })
});

const data = await response.json();
python
import requests

response = requests.delete(
    'https://api.probiddr.com/bids/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "reason": "Found a conflict in my schedule"
    }
)

data = response.json()
Responses
200 OK
Bid withdrawn successfully
json
{
  "bid": {
    "id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
    "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
    "contractor_id": "ctr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "status": "pending",
    "amount": 7500,
    "estimated_duration_days": 21,
    "message": "I have 15 years of experience in kitchen remodeling and can start next week.",
    "proposed_start_date": "2025-02-01",
    "materials_included": true,
    "payment_terms": "50% upfront, 50% on completion",
    "guaranteed_view": false,
    "hold_id": "hld_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "created_at": "2025-01-15T10:30:00Z",
    "updated_at": "2025-01-15T14:20:00Z",
    "submitted_at": "2025-01-15T11:00:00Z"
  },
  "tokens_refunded": 5,
  "hold_released": false
}
400 Bad Request
Invalid transition
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /bids/{id}/submit
Submit a draft bid, making it visible to the homeowner and charging tokens. **State transition:** draft β†’ pending **Contractor-only action.** **Token cost (deducted on submission):** - standard urgency: 5 tokens - priority urgency: 15 tokens - urgent urgency: 25 tokens **Guaranteed view feature (optional):** - Cost: 20 token hold (not deducted immediately) - Ensures homeowner will view the bid - Hold released when: - Homeowner views the bid (pending β†’ viewed transition) - 24 hours pass without viewing (automatic release, charge applied) - Tokens only deducted if homeowner doesn't view within 24h **Required fields to submit:** - All draft fields must be complete - Sufficient token balance for submission + guaranteed_view (if requested) **Validation:** - Bid must be in draft status - All required fields present - Sufficient token balance - Job still in 'open' status
Parameters
Name Type Description
id required
Path
string Bid KSUID identifier
Request Body
Parameter Type Description
guaranteed_view boolean Request guaranteed view (20 token hold)
Example Request
bash
curl -X POST https://api.probiddr.com/bids/{id}/submit \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "guaranteed_view": false
  }'
javascript
const response = await fetch('https://api.probiddr.com/bids/{id}/submit', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "guaranteed_view": false
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/bids/{id}/submit',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "guaranteed_view": false
    }
)

data = response.json()
Responses
200 OK
Bid submitted successfully
json
{
  "bid": {
    "id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
    "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
    "contractor_id": "ctr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "status": "pending",
    "amount": 7500,
    "estimated_duration_days": 21,
    "message": "I have 15 years of experience in kitchen remodeling and can start next week.",
    "proposed_start_date": "2025-02-01",
    "materials_included": true,
    "payment_terms": "50% upfront, 50% on completion",
    "guaranteed_view": false,
    "hold_id": "hld_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "created_at": "2025-01-15T10:30:00Z",
    "updated_at": "2025-01-15T14:20:00Z",
    "submitted_at": "2025-01-15T11:00:00Z"
  },
  "tokens_charged": 5,
  "guaranteed_view_hold": 0,
  "hold_id": null,
  "remaining_balance": 20
}
400 Bad Request
Validation error or insufficient tokens
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /bids/{id}/accept
Accept a bid and award the job to the contractor. **State transition:** pending/viewed/shortlisted β†’ accepted **Homeowner-only action.** **Side effects:** - Bid status: * β†’ accepted - Job status: open β†’ awarded - Job's awarded_bid_id field updated - All other bids for this job: * β†’ rejected - Guaranteed view hold released (if applicable) - `bid.accepted` event emitted - `job.awarded` event emitted **Business rules:** - Can only accept bids on your own jobs - Job must be in 'open' status - Bid must be in pending, viewed, or shortlisted status
Parameters
Name Type Description
id required
Path
string Bid KSUID identifier
Request Body
Parameter Type Description
notes string Optional notes about acceptance
Example Request
bash
curl -X POST https://api.probiddr.com/bids/{id}/accept \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "notes": "Looking forward to working with you!"
  }'
javascript
const response = await fetch('https://api.probiddr.com/bids/{id}/accept', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "notes": "Looking forward to working with you!"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/bids/{id}/accept',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "notes": "Looking forward to working with you!"
    }
)

data = response.json()
Responses
200 OK
Bid accepted successfully
json
{
  "bid": {
    "id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
    "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
    "contractor_id": "ctr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "status": "pending",
    "amount": 7500,
    "estimated_duration_days": 21,
    "message": "I have 15 years of experience in kitchen remodeling and can start next week.",
    "proposed_start_date": "2025-02-01",
    "materials_included": true,
    "payment_terms": "50% upfront, 50% on completion",
    "guaranteed_view": false,
    "hold_id": "hld_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "created_at": "2025-01-15T10:30:00Z",
    "updated_at": "2025-01-15T14:20:00Z",
    "submitted_at": "2025-01-15T11:00:00Z"
  },
  "job": {
    "id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
    "title": "Kitchen Remodel",
    "description": "Complete kitchen renovation including cabinets, countertops, and appliances",
    "status": "open",
    "category": "Remodeling",
    "budget_min": 5000,
    "budget_max": 10000,
    "urgency": "standard",
    "address_line1": "123 Main St",
    "address_line2": "Apt 4B",
    "city": "San Francisco",
    "state": "CA",
    "zip_code": "94103",
    "country": "USA",
    "latitude": 37.7749,
    "longitude": -122.4194,
    "desired_start_date": "2025-02-01",
    "desired_completion_date": "2025-03-15",
    "requirements": "Must be licensed and insured",
    "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "awarded_bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
    "created_at": "2025-01-15T10:30:00Z",
    "updated_at": "2025-01-15T14:20:00Z"
  },
  "other_bids_rejected": 4
}
400 Bad Request
Invalid transition
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Not authorized - not the job owner
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /bids/{id}/reject
Reject a bid. **State transition:** pending/viewed/shortlisted β†’ rejected **Homeowner-only action.** **Side effects:** - Bid status: * β†’ rejected - Guaranteed view hold released (if applicable, charged) - `bid.rejected` event emitted **Business rules:** - Can only reject bids on your own jobs - Bid must be in pending, viewed, or shortlisted status - Once rejected, bid cannot be un-rejected (terminal state)
Parameters
Name Type Description
id required
Path
string Bid KSUID identifier
Request Body
Parameter Type Description
reason string Optional reason for rejection
Example Request
bash
curl -X POST https://api.probiddr.com/bids/{id}/reject \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Bid amount exceeds budget"
  }'
javascript
const response = await fetch('https://api.probiddr.com/bids/{id}/reject', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "reason": "Bid amount exceeds budget"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/bids/{id}/reject',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "reason": "Bid amount exceeds budget"
    }
)

data = response.json()
Responses
200 OK
Bid rejected successfully
json
{
  "id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "contractor_id": "ctr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "status": "pending",
  "amount": 7500,
  "estimated_duration_days": 21,
  "message": "I have 15 years of experience in kitchen remodeling and can start next week.",
  "proposed_start_date": "2025-02-01",
  "materials_included": true,
  "payment_terms": "50% upfront, 50% on completion",
  "guaranteed_view": false,
  "hold_id": "hld_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z",
  "submitted_at": "2025-01-15T11:00:00Z"
}
400 Bad Request
Invalid transition
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Not authorized - not the job owner
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /conversations
Retrieve a paginated list of conversations for the authenticated user. **Access control:** - Users see only their own conversations - Admin: Can view all conversations **Sorting:** - Default: Most recent message first - Shows conversation preview with last message timestamp **Business context:** - Conversations are automatically created when a bid is accepted - Users can also create direct conversations outside of bid context
Parameters
Name Type Description
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
has_unread
Query
boolean Filter to conversations with unread messages
Example Request
bash
curl -X GET https://api.probiddr.com/conversations \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/conversations', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/conversations',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
List of conversations retrieved successfully
json
{}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /conversations/{id}
Retrieve detailed information about a specific conversation. **Access control:** - Users can view only conversations they are part of - Admin: Can view all conversations **Side effects:** - Does not mark messages as read (use GET /conversations/{id}/messages for that)
Parameters
Name Type Description
id required
Path
string Conversation KSUID identifier
Example Request
bash
curl -X GET https://api.probiddr.com/conversations/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/conversations/{id}', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/conversations/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Conversation details retrieved successfully
json
{
  "id": "cvs_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "contractor_id": "ctr_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "last_message_at": "2025-01-15T14:20:00Z",
  "unread_count_homeowner": 2,
  "unread_count_contractor": 0,
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /conversations/{id}/messages
Retrieve all messages in a specific conversation. **Access control:** - Users can view only conversations they are part of - Admin: Can view all conversations **Side effects:** - Automatically marks all messages in this conversation as read for the authenticated user - Updates conversation's unread count for this user **Sorting:** - Default: Oldest first (chronological order) - Use offset/limit for pagination through long message histories
Parameters
Name Type Description
id required
Path
string Conversation KSUID identifier
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
Example Request
bash
curl -X GET https://api.probiddr.com/conversations/{id}/messages \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/conversations/{id}/messages', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/conversations/{id}/messages',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Messages retrieved successfully
json
{}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /conversations/{id}/messages
Send a message in an existing conversation. **User must be participant in the conversation.** **Side effects:** - Updates conversation's last_message_at timestamp - Increments unread count for recipient - Emits `message.sent` event for real-time notifications **Message limits:** - Maximum 2000 characters per message - Rate limited to prevent spam
Parameters
Name Type Description
id required
Path
string Conversation KSUID identifier
Request Body
Parameter Type Description
message required string Message text content
Example Request
bash
curl -X POST https://api.probiddr.com/conversations/{id}/messages \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "I have a question about the project timeline."
  }'
javascript
const response = await fetch('https://api.probiddr.com/conversations/{id}/messages', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "message": "I have a question about the project timeline."
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/conversations/{id}/messages',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "message": "I have a question about the project timeline."
    }
)

data = response.json()
Responses
201 Created
Message sent successfully
json
{
  "id": "msg_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "conversation_id": "cvs_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "sender_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "recipient_id": "usr_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "content": "When can you start the project?",
  "read_at": "2025-01-15T15:30:00Z",
  "created_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Not a participant in this conversation
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /conversations/bid/{bid_id}
Retrieve the conversation associated with a specific bid. **Access control:** - Users can view only conversations they are part of - Admin: Can view all conversations **Use case:** - Useful when you have a bid_id and need to access the conversation - Returns 404 if no conversation exists for this bid (bid not yet accepted)
Parameters
Name Type Description
bid_id required
Path
string Bid KSUID identifier
Example Request
bash
curl -X GET https://api.probiddr.com/conversations/bid/{bid_id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/conversations/bid/{bid_id}', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/conversations/bid/{bid_id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Conversation retrieved successfully
json
{
  "id": "cvs_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "contractor_id": "ctr_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "last_message_at": "2025-01-15T14:20:00Z",
  "unread_count_homeowner": 2,
  "unread_count_contractor": 0,
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
No conversation found for this bid
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /messages/unread-count
Get the total number of unread messages across all conversations for the authenticated user. **Access control:** - Users see only their own unread count **Use case:** - Display notification badge in UI - Check for new messages without fetching full conversation list
Example Request
bash
curl -X GET https://api.probiddr.com/messages/unread-count \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/messages/unread-count', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/messages/unread-count',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Unread count retrieved successfully
json
{
  "unread_count": 5,
  "conversations_with_unread": 2
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /messages
Create a new conversation and send the first message. **Business rules:** - Can only message users you have a business relationship with: - Homeowners can message contractors who have bid on their jobs - Contractors can message homeowners whose jobs they've bid on - Admin can message anyone **Side effects:** - Creates a new conversation if one doesn't exist - Sends the message in the conversation - Emits `conversation.created` and `message.sent` events
Request Body
Parameter Type Description
recipient_id required string User ID of the recipient
job_id string Optional job context for the conversation
message required string Message text content
Example Request
bash
curl -X POST https://api.probiddr.com/messages \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "recipient_id": "usr_3ADMbYYNYyyF5VHbDyG1UbsvJmC",
    "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
    "message": "I'd like to discuss the project details with you."
  }'
javascript
const response = await fetch('https://api.probiddr.com/messages', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "recipient_id": "usr_3ADMbYYNYyyF5VHbDyG1UbsvJmC",
    "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
    "message": "I'd like to discuss the project details with you."
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/messages',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "recipient_id": "usr_3ADMbYYNYyyF5VHbDyG1UbsvJmC",
      "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
      "message": "I'd like to discuss the project details with you."
    }
)

data = response.json()
Responses
201 Created
Conversation created and message sent successfully
json
{
  "conversation": {
    "id": "cvs_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "bid_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
    "job_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
    "homeowner_id": "hmw_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "contractor_id": "ctr_3ADMbZGjK82Ohs0MMYXlqMuECpc",
    "last_message_at": "2025-01-15T14:20:00Z",
    "unread_count_homeowner": 2,
    "unread_count_contractor": 0,
    "created_at": "2025-01-15T10:30:00Z",
    "updated_at": "2025-01-15T14:20:00Z"
  },
  "message": {
    "id": "msg_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "conversation_id": "cvs_3ADMbZGjK82Ohs0MMYXlqMuECpc",
    "sender_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "recipient_id": "usr_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
    "content": "When can you start the project?",
    "read_at": "2025-01-15T15:30:00Z",
    "created_at": "2025-01-15T14:20:00Z"
  }
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Not allowed to message this user
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Recipient not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /messages/mark-read
Mark multiple messages as read in a single request. **Access control:** - Users can only mark their own received messages as read **Side effects:** - Updates is_read and read_at for all specified messages - Updates conversation unread counts - Emits `message.read` events for real-time status updates **Use case:** - Mark all messages in a conversation as read - Mark messages from multiple conversations as read at once
Request Body
Parameter Type Description
message_ids required array Array of message IDs to mark as read
Example Request
bash
curl -X POST https://api.probiddr.com/messages/mark-read \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "message_ids": [
      "msg_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
      "msg_3ADMbYYNYyyF5VHbDyG1UbsvJmC"
    ]
  }'
javascript
const response = await fetch('https://api.probiddr.com/messages/mark-read', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "message_ids": [
      "msg_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
      "msg_3ADMbYYNYyyF5VHbDyG1UbsvJmC"
    ]
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/messages/mark-read',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "message_ids": [
        "msg_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
        "msg_3ADMbYYNYyyF5VHbDyG1UbsvJmC"
      ]
    }
)

data = response.json()
Responses
200 OK
Messages marked as read successfully
json
{
  "marked_count": 2
}
400 Bad Request
Invalid request
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Not authorized to mark these messages as read
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
PUT /messages/{id}/read
Mark a specific message as read. **Access control:** - Users can only mark their own received messages as read **Side effects:** - Updates is_read and read_at for the message - Updates conversation unread count - Emits `message.read` event for real-time status updates
Parameters
Name Type Description
id required
Path
string Message KSUID identifier
Example Request
bash
curl -X PUT https://api.probiddr.com/messages/{id}/read \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/messages/{id}/read', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.put(
    'https://api.probiddr.com/messages/{id}/read',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Message marked as read successfully
json
{
  "id": "msg_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "conversation_id": "cvs_3ADMbZGjK82Ohs0MMYXlqMuECpc",
  "sender_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "recipient_id": "usr_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "content": "When can you start the project?",
  "read_at": "2025-01-15T15:30:00Z",
  "created_at": "2025-01-15T14:20:00Z"
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Not the recipient of this message
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
DELETE /messages/{id}
Delete a message from a conversation. **Access control:** - Users can only delete messages they sent - Admin can delete any message **Business rules:** - Soft delete (message marked as deleted, not removed from database) - Message content replaced with "[Message deleted]" - Cannot delete messages older than 24 hours (except admin) **Side effects:** - Updates conversation's last_message_at if this was the most recent message - Emits `message.deleted` event for real-time updates
Parameters
Name Type Description
id required
Path
string Message KSUID identifier
Example Request
bash
curl -X DELETE https://api.probiddr.com/messages/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/messages/{id}', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.delete(
    'https://api.probiddr.com/messages/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Message deleted successfully
json
{
  "status": 200,
  "message": "Operation completed successfully",
  "data": {}
}
400 Bad Request
Cannot delete message
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Not the message sender
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /tokens/balance
Retrieve the token balance for the authenticated user. **Balance components:** - **available**: Tokens available for spending (total - held) - **held**: Tokens currently held in escrow (guaranteed_view holds) - **total**: Total token balance (available + held) **Use case:** - Check balance before posting jobs or submitting bids - Display balance in user dashboard
Example Request
bash
curl -X GET https://api.probiddr.com/tokens/balance \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/tokens/balance', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/tokens/balance',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Token balance retrieved successfully
json
{
  "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "available": 45,
  "held": 20,
  "total": 65
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /tokens/transactions
Retrieve paginated transaction history for the authenticated user. **Transaction types:** - **credit**: Tokens added to account (welcome bonus, purchase, referral, refund) - **debit**: Tokens deducted from account (job posting, bid submission) - **hold**: Tokens held in escrow (guaranteed_view) - **release**: Tokens released from hold (hold expired or captured) **Filtering:** - Filter by transaction type - Filter by date range **Sorting:** - Default: Most recent first
Parameters
Name Type Description
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
type
Query
string Filter by transaction type
from_date
Query
string Filter transactions from this date (ISO 8601)
to_date
Query
string Filter transactions until this date (ISO 8601)
Example Request
bash
curl -X GET https://api.probiddr.com/tokens/transactions \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/tokens/transactions', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/tokens/transactions',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Transaction history retrieved successfully
json
{}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /tokens/packages
Retrieve available token packages for purchase. **Package tiers:** - Starter: Small token bundle for new users - Standard: Popular mid-tier bundle with 5% bonus - Premium: Large bundle with 10% bonus - Business: Bulk bundle for active users with 15% bonus **Pricing:** - Dynamic pricing based on current market rates - Includes bonus tokens for larger purchases **Public endpoint** (no authentication required).
Example Request
bash
curl -X GET https://api.probiddr.com/tokens/packages \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/tokens/packages', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/tokens/packages',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Token packages retrieved successfully
json
{
  "packages": [
    {
      "id": "pkg_starter",
      "name": "Starter Pack",
      "tokens": 50,
      "bonus_tokens": 0,
      "total_tokens": 50,
      "price_usd": 9.99,
      "stripe_price_id": "price_1234567890"
    }
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /tokens/holds
Retrieve all active token holds for the authenticated user. **Hold types:** - **guaranteed_view**: Tokens held for guaranteed bid visibility (20 tokens) **Hold lifecycle:** - Created when feature is requested - Released when condition is met (bid viewed) or expires (24 hours) - Captured if condition not met (tokens deducted) **Filtering:** - Filter by hold status (active, released, captured)
Parameters
Name Type Description
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
status
Query
string Filter by hold status
Example Request
bash
curl -X GET https://api.probiddr.com/tokens/holds \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/tokens/holds', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/tokens/holds',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Active holds retrieved successfully
json
{}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /tokens/holds
Create a new token hold (escrow). **Use cases:** - Guaranteed view for bids (20 tokens) - Future: Job deposits, milestone payments **Business rules:** - User must have sufficient available balance - Hold automatically expires after specified duration - Hold can be released (no charge) or captured (charge applied) **Note:** In normal flow, holds are created automatically by bid submission. This endpoint is for admin/debugging purposes.
Request Body
Parameter Type Description
amount required integer Number of tokens to hold
type required string Hold type
reference_type required string Type of related entity
reference_id required string ID of related entity
expires_in_hours integer Hours until hold expires
Example Request
bash
curl -X POST https://api.probiddr.com/tokens/holds \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 20,
    "type": "guaranteed_view",
    "reference_type": "bid",
    "reference_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
    "expires_in_hours": 24
  }'
javascript
const response = await fetch('https://api.probiddr.com/tokens/holds', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "amount": 20,
    "type": "guaranteed_view",
    "reference_type": "bid",
    "reference_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
    "expires_in_hours": 24
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/tokens/holds',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "amount": 20,
      "type": "guaranteed_view",
      "reference_type": "bid",
      "reference_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
      "expires_in_hours": 24
    }
)

data = response.json()
Responses
201 Created
Hold created successfully
json
{
  "id": "hld_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "amount": 20,
  "reason": "Guaranteed bid view",
  "reference_type": "bid",
  "reference_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "status": "active",
  "expires_at": "2025-01-16T14:20:00Z",
  "created_at": "2025-01-15T14:20:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Insufficient balance or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /tokens/purchase
Purchase a token package using Stripe payment. **Payment flow:** 1. Select package from GET /tokens/packages 2. Call this endpoint to create Stripe checkout session 3. Redirect user to Stripe checkout URL 4. After successful payment, Stripe webhook credits tokens to user account 5. User redirected back to success_url **Idempotency:** - Use idempotency_key to prevent duplicate charges - Same key returns existing session if still active **Side effects:** - Creates Stripe checkout session - On successful payment: Credits tokens + bonus tokens to account - Emits `tokens.purchased` event
Request Body
Parameter Type Description
package_id required string Package identifier from GET /tokens/packages
success_url required string URL to redirect after successful payment
cancel_url required string URL to redirect if user cancels
idempotency_key string Optional idempotency key to prevent duplicate charges
Example Request
bash
curl -X POST https://api.probiddr.com/tokens/purchase \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "package_id": "pkg_standard",
    "success_url": "https://probiddr.com/tokens/success",
    "cancel_url": "https://probiddr.com/tokens/cancel",
    "idempotency_key": "usr_3ADMb_1737024000"
  }'
javascript
const response = await fetch('https://api.probiddr.com/tokens/purchase', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "package_id": "pkg_standard",
    "success_url": "https://probiddr.com/tokens/success",
    "cancel_url": "https://probiddr.com/tokens/cancel",
    "idempotency_key": "usr_3ADMb_1737024000"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/tokens/purchase',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "package_id": "pkg_standard",
      "success_url": "https://probiddr.com/tokens/success",
      "cancel_url": "https://probiddr.com/tokens/cancel",
      "idempotency_key": "usr_3ADMb_1737024000"
    }
)

data = response.json()
Responses
200 OK
Stripe checkout session created successfully
json
{
  "checkout_url": "https://checkout.stripe.com/c/pay/cs_test_...",
  "session_id": "cs_test_1234567890",
  "package": {
    "id": "pkg_standard",
    "total_tokens": 105,
    "price_usd": 19.99
  }
}
400 Bad Request
Invalid package or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /tokens/credit
Credit tokens to a user's account. **Admin-only action.** **Use cases:** - Manual adjustments - Customer service credits - Promotional bonuses - Compensation for issues **Side effects:** - Adds tokens to user's available balance - Records transaction in transaction history - Emits `tokens.credited` event **Idempotency:** - Use idempotency_key to prevent duplicate credits
Request Body
Parameter Type Description
user_id required string User to credit tokens to
amount required integer Number of tokens to credit
description required string Reason for credit
idempotency_key string Optional idempotency key
Example Request
bash
curl -X POST https://api.probiddr.com/tokens/credit \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "amount": 50,
    "description": "Compensation for service issue",
    "idempotency_key": "admin_credit_20250120_001"
  }'
javascript
const response = await fetch('https://api.probiddr.com/tokens/credit', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "amount": 50,
    "description": "Compensation for service issue",
    "idempotency_key": "admin_credit_20250120_001"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/tokens/credit',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
      "amount": 50,
      "description": "Compensation for service issue",
      "idempotency_key": "admin_credit_20250120_001"
    }
)

data = response.json()
Responses
200 OK
Tokens credited successfully
json
{
  "transaction": {
    "id": "txn_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "type": "debit",
    "amount": 5,
    "balance_after": 45,
    "reason": "Job posting (standard urgency)",
    "reference_type": "job",
    "reference_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
    "idempotency_key": "job-post-123-abc",
    "created_at": "2025-01-15T14:20:00Z"
  },
  "new_balance": 95
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
User not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /tokens/debit
Debit tokens from a user's account. **Admin-only action.** **Use cases:** - Manual corrections - Fraud adjustments - Policy violation penalties **Business rules:** - User must have sufficient available balance - Cannot debit more than available balance **Side effects:** - Deducts tokens from user's available balance - Records transaction in transaction history - Emits `tokens.debited` event **Idempotency:** - Use idempotency_key to prevent duplicate debits
Request Body
Parameter Type Description
user_id required string User to debit tokens from
amount required integer Number of tokens to debit
description required string Reason for debit
idempotency_key string Optional idempotency key
Example Request
bash
curl -X POST https://api.probiddr.com/tokens/debit \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "amount": 10,
    "description": "Manual correction for duplicate credit",
    "idempotency_key": "admin_debit_20250120_001"
  }'
javascript
const response = await fetch('https://api.probiddr.com/tokens/debit', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "amount": 10,
    "description": "Manual correction for duplicate credit",
    "idempotency_key": "admin_debit_20250120_001"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/tokens/debit',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
      "amount": 10,
      "description": "Manual correction for duplicate credit",
      "idempotency_key": "admin_debit_20250120_001"
    }
)

data = response.json()
Responses
200 OK
Tokens debited successfully
json
{
  "transaction": {
    "id": "txn_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "type": "debit",
    "amount": 5,
    "balance_after": 45,
    "reason": "Job posting (standard urgency)",
    "reference_type": "job",
    "reference_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
    "idempotency_key": "job-post-123-abc",
    "created_at": "2025-01-15T14:20:00Z"
  },
  "new_balance": 85
}
400 Bad Request
Insufficient balance or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
User not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /tokens/holds/{id}/capture
Capture a token hold, converting it to a debit transaction. **Admin-only action.** **State transition:** active β†’ captured **Use case:** - Charge tokens when hold condition not met - Example: Guaranteed view not delivered within 24 hours **Side effects:** - Hold status: active β†’ captured - Deducts tokens from user's available balance - Records debit transaction in history - Emits `tokens.hold.captured` event **Note:** Normally holds are captured automatically by background jobs. This endpoint is for manual/emergency operations.
Parameters
Name Type Description
id required
Path
string Hold KSUID identifier
Example Request
bash
curl -X POST https://api.probiddr.com/tokens/holds/{id}/capture \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/tokens/holds/{id}/capture', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/tokens/holds/{id}/capture',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Hold captured successfully
json
{
  "hold": {
    "id": "hld_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "amount": 20,
    "reason": "Guaranteed bid view",
    "reference_type": "bid",
    "reference_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
    "status": "active",
    "expires_at": "2025-01-16T14:20:00Z",
    "created_at": "2025-01-15T14:20:00Z",
    "updated_at": "2025-01-15T14:20:00Z"
  },
  "transaction": {
    "id": "txn_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "type": "debit",
    "amount": 5,
    "balance_after": 45,
    "reason": "Job posting (standard urgency)",
    "reference_type": "job",
    "reference_id": "job_3ADMbZGjK82Ohs0MMYXlqMuECpc",
    "idempotency_key": "job-post-123-abc",
    "created_at": "2025-01-15T14:20:00Z"
  }
}
400 Bad Request
Hold cannot be captured
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /tokens/holds/{id}/release
Release a token hold without charging the user. **Admin-only action.** **State transition:** active β†’ released **Use case:** - Release hold when condition is met - Example: Guaranteed view delivered successfully **Side effects:** - Hold status: active β†’ released - Tokens returned to user's available balance - No debit transaction created - Emits `tokens.hold.released` event **Note:** Normally holds are released automatically by the system. This endpoint is for manual/emergency operations.
Parameters
Name Type Description
id required
Path
string Hold KSUID identifier
Example Request
bash
curl -X POST https://api.probiddr.com/tokens/holds/{id}/release \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/tokens/holds/{id}/release', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/tokens/holds/{id}/release',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Hold released successfully
json
{
  "id": "hld_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "amount": 20,
  "reason": "Guaranteed bid view",
  "reference_type": "bid",
  "reference_id": "bid_3ADMbQcjgDbLfVS7dkK8Qmnmk18",
  "status": "active",
  "expires_at": "2025-01-16T14:20:00Z",
  "created_at": "2025-01-15T14:20:00Z",
  "updated_at": "2025-01-15T14:20:00Z"
}
400 Bad Request
Hold cannot be released
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /referrals/codes
Retrieve all referral codes created by the authenticated user. **Access control:** - Users see only their own referral codes - Admin: Can view all referral codes **Referral code format:** - Alphanumeric code (8-12 characters) - Case-insensitive - Unique across platform **Code types:** - **personal**: For sharing with friends/family - **promotional**: Custom promotional codes (admin-created) **Usage tracking:** - Each code includes usage count and reward totals
Parameters
Name Type Description
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
Example Request
bash
curl -X GET https://api.probiddr.com/referrals/codes \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/referrals/codes', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/referrals/codes',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Referral codes retrieved successfully
json
{}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /referrals/codes
Create a new referral code for the authenticated user. **Business rules:** - Users can create multiple referral codes - Code must be unique across platform - Code must be 8-12 alphanumeric characters - Custom codes allowed (subject to availability) - System generates random code if not provided **Referral rewards:** - Referrer: 10 tokens when referred user completes first action - Referred: 10 tokens bonus on signup (on top of welcome bonus) **First action triggers:** - Homeowner: Posts first job - Contractor: Submits first bid
Request Body
Parameter Type Description
code string Custom referral code (optional - auto-generated if not provided)
Example Request
bash
curl -X POST https://api.probiddr.com/referrals/codes \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "JOHNDOE123"
  }'
javascript
const response = await fetch('https://api.probiddr.com/referrals/codes', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "code": "JOHNDOE123"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/referrals/codes',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "code": "JOHNDOE123"
    }
)

data = response.json()
Responses
201 Created
Referral code created successfully
json
{
  "id": "ref_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "code": "JOHN2025",
  "uses": 5,
  "created_at": "2025-01-15T10:30:00Z"
}
400 Bad Request
Validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /referrals/stats
Retrieve referral statistics for the authenticated user. **Statistics include:** - Total referrals (users who signed up with your code) - Active referrals (referred users who completed first action) - Total tokens earned from referrals - Conversion rate (active / total referrals) - Breakdown by user type (homeowner vs contractor) **Use case:** - Display referral performance in user dashboard - Track referral program effectiveness
Example Request
bash
curl -X GET https://api.probiddr.com/referrals/stats \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/referrals/stats', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/referrals/stats',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Referral statistics retrieved successfully
json
{
  "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "total_referrals": 12,
  "total_tokens_earned": 120,
  "referrals_this_month": 3,
  "referrals_this_year": 12
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /referrals/rewards
Retrieve history of tokens earned from referrals. **Reward lifecycle:** - **pending**: Referred user signed up but hasn't completed first action - **earned**: Referred user completed first action, tokens credited - **expired**: Referred user never completed first action (30 day timeout) **Each reward entry includes:** - Referred user's info (anonymized if privacy settings) - Date of signup and first action - Token amount earned - Referral code used
Parameters
Name Type Description
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
status
Query
string Filter by reward status
Example Request
bash
curl -X GET https://api.probiddr.com/referrals/rewards \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/referrals/rewards', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/referrals/rewards',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Referral rewards retrieved successfully
json
{}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /referrals/apply
Apply a referral code during or after user signup. **Timing:** - Best practice: Apply during signup flow - Can be applied retroactively within 24 hours of signup - Cannot apply after first action (job post or bid submission) **Business rules:** - User can only apply one referral code per account - Cannot apply own referral code - Referred user receives 10 token bonus immediately - Referrer receives 10 tokens when referred user completes first action **Side effects:** - Credits 10 tokens to referred user's account - Creates pending reward for referrer - Emits `referral.applied` event
Request Body
Parameter Type Description
code required string Referral code to apply
Example Request
bash
curl -X POST https://api.probiddr.com/referrals/apply \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "JOHNDOE123"
  }'
javascript
const response = await fetch('https://api.probiddr.com/referrals/apply', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "code": "JOHNDOE123"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/referrals/apply',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "code": "JOHNDOE123"
    }
)

data = response.json()
Responses
200 OK
Referral code applied successfully
json
{
  "message": "Referral code applied successfully",
  "bonus_tokens": 10,
  "referrer_reward": "Your referrer will receive 10 tokens when you complete your first action"
}
400 Bad Request
Validation error or code cannot be applied
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /sessions
Retrieve a list of all active sessions for the authenticated user. **Session information includes:** - Device type (mobile, desktop, tablet) - Browser and operating system - IP address (last 3 octets masked for privacy) - Geographic location (city/country level) - Last activity timestamp - Current session indicator **Use cases:** - Security audit: Review active sessions - Device management: See where you're logged in - Suspicious activity detection **Session lifecycle:** - Sessions created on login - Expire after 30 days of inactivity - Can be manually revoked via DELETE endpoint
Parameters
Name Type Description
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
Example Request
bash
curl -X GET https://api.probiddr.com/sessions \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/sessions', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/sessions',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Active sessions retrieved successfully
json
{}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
DELETE /sessions/{token_id}
Revoke a specific session, logging out that device. **Access control:** - Users can only revoke their own sessions - Cannot revoke the current session (use logout endpoint instead) **Side effects:** - Invalidates the session token - Device must re-authenticate to access API - Emits `session.revoked` event for security audit **Use cases:** - Remote logout from lost/stolen device - Revoke access from shared computer - Security cleanup after suspicious activity **Note:** To logout the current session, use the standard logout endpoint.
Parameters
Name Type Description
token_id required
Path
string Session token ID to revoke
Example Request
bash
curl -X DELETE https://api.probiddr.com/sessions/{token_id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/sessions/{token_id}', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.delete(
    'https://api.probiddr.com/sessions/{token_id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Session revoked successfully
json
{
  "status": 200,
  "message": "Operation completed successfully",
  "data": {}
}
400 Bad Request
Cannot revoke current session
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Not authorized to revoke this session
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Session not found or already revoked
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /subscriptions
Retrieve email subscription preferences for the authenticated user. **Subscription categories:** **Homeowners:** - `job_updates`: Notifications about your jobs (bids, messages, status changes) - `bid_notifications`: New bids on your jobs - `message_notifications`: New messages in conversations - `marketing`: Promotional emails and platform updates - `digest`: Weekly summary of activity **Contractors:** - `new_jobs`: Notifications about new job postings in your area - `bid_updates`: Status changes on your bids - `message_notifications`: New messages in conversations - `job_matches`: Jobs matching your specialties and service areas - `marketing`: Promotional emails and platform updates - `digest`: Weekly summary of activity **Transactional emails:** - Cannot be disabled (account security, password resets, etc.) **Note:** All users have default preferences initialized on signup.
Example Request
bash
curl -X GET https://api.probiddr.com/subscriptions \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/subscriptions', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/subscriptions',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Subscription preferences retrieved successfully
json
{
  "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user_type": "homeowner",
  "subscriptions": {
    "job_status_updates": true,
    "new_bids": true,
    "messages": true,
    "marketing": false
  }
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
PUT /subscriptions
Update email subscription preferences for the authenticated user. **Updateable preferences:** - All notification and marketing preferences - Cannot modify transactional email settings **Partial updates:** - Only include fields you want to change - Omitted fields remain unchanged **Side effects:** - Updates user's subscription preferences in database - Emits `subscriptions.updated` event - Changes take effect immediately for future emails
Request Body
Parameter Type Description
job_updates boolean Homeowner only - Job status updates
bid_notifications boolean Homeowner only - New bid notifications
new_jobs boolean Contractor only - New job alerts
bid_updates boolean Contractor only - Bid status updates
job_matches boolean Contractor only - Matching job alerts
message_notifications boolean Both - New message notifications
marketing boolean Both - Marketing and promotional emails
digest boolean Both - Weekly digest emails
Example Request
bash
curl -X PUT https://api.probiddr.com/subscriptions \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "job_updates": true,
    "bid_notifications": true,
    "new_jobs": true,
    "bid_updates": true,
    "job_matches": true,
    "message_notifications": true,
    "marketing": false,
    "digest": true
  }'
javascript
const response = await fetch('https://api.probiddr.com/subscriptions', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "job_updates": true,
    "bid_notifications": true,
    "new_jobs": true,
    "bid_updates": true,
    "job_matches": true,
    "message_notifications": true,
    "marketing": false,
    "digest": true
  })
});

const data = await response.json();
python
import requests

response = requests.put(
    'https://api.probiddr.com/subscriptions',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "job_updates": true,
      "bid_notifications": true,
      "new_jobs": true,
      "bid_updates": true,
      "job_matches": true,
      "message_notifications": true,
      "marketing": false,
      "digest": true
    }
)

data = response.json()
Responses
200 OK
Subscription preferences updated successfully
json
{
  "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user_type": "homeowner",
  "subscriptions": {
    "job_status_updates": true,
    "new_bids": true,
    "messages": true,
    "marketing": false
  }
}
400 Bad Request
Invalid subscription key for user type
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /subscriptions/options/{type}
Retrieve available subscription options for a specific user type. **Use case:** - Populate subscription preferences form in UI - Display all available toggles for user type - Show descriptions and categories **Public endpoint** (no authentication required). Returns metadata about each subscription type including: - Key name - Display name - Description - Category (notifications, marketing, digest) - Default value - Whether it's transactional (cannot be disabled)
Parameters
Name Type Description
type required
Path
string User type
Example Request
bash
curl -X GET https://api.probiddr.com/subscriptions/options/{type} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/subscriptions/options/{type}', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/subscriptions/options/{type}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Subscription options retrieved successfully
json
{
  "user_type": "homeowner",
  "options": [
    {
      "key": "job_updates",
      "display_name": "Job Updates",
      "description": "Notifications about status changes on your jobs",
      "category": "notifications",
      "default_value": true,
      "can_disable": true
    }
  ]
}
400 Bad Request
Invalid user type
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /subscriptions/initialize
Initialize default subscription preferences for a new user. **Use case:** - Called automatically during user signup process - Can be called manually if preferences not initialized - Idempotent - safe to call multiple times **Side effects:** - Creates subscription preferences with defaults for user type - No-op if preferences already exist - Emits `subscriptions.initialized` event **Default values:** - All notification preferences: enabled - Marketing: enabled - Digest: enabled - Transactional: always enabled (not stored in preferences)
Request Body
Parameter Type Description
user_id required string User KSUID
user_type required string User type determines available subscriptions
Example Request
bash
curl -X POST https://api.probiddr.com/subscriptions/initialize \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "user_type": "homeowner"
  }'
javascript
const response = await fetch('https://api.probiddr.com/subscriptions/initialize', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "user_type": "homeowner"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/subscriptions/initialize',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
      "user_type": "homeowner"
    }
)

data = response.json()
Responses
200 OK
Subscription preferences initialized (or already existed)
json
{
  "subscription": {
    "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "user_type": "homeowner",
    "subscriptions": {
      "job_status_updates": true,
      "new_bids": true,
      "messages": true,
      "marketing": false
    }
  },
  "created": true
}
400 Bad Request
Invalid user type or user not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /subscriptions/unsubscribe
Unsubscribe from email category using token from email footer link. **Public endpoint** (no authentication required) - uses encrypted token. **Email footer includes:** - Unsubscribe link with encrypted token - Token contains: user_id, email, category, expiry - Token valid for 30 days **Categories:** - `all_marketing`: Unsubscribe from all marketing emails - `digest`: Unsubscribe from weekly digest - `specific`: Unsubscribe from specific notification type **Side effects:** - Updates subscription preferences - Cannot unsubscribe from transactional emails - Redirects to confirmation page **Token security:** - AES-256-GCM encrypted - Single-use tokens (optional) - Expires after 30 days
Request Body
Parameter Type Description
token required string Encrypted unsubscribe token from email link
Example Request
bash
curl -X POST https://api.probiddr.com/subscriptions/unsubscribe \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  }'
javascript
const response = await fetch('https://api.probiddr.com/subscriptions/unsubscribe', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/subscriptions/unsubscribe',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    }
)

data = response.json()
Responses
200 OK
Unsubscribed successfully
json
{
  "message": "You have been unsubscribed from marketing emails",
  "unsubscribed_from": [
    "marketing"
  ],
  "redirect_url": "https://probiddr.com/unsubscribe/success"
}
400 Bad Request
Invalid or expired token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /notifications
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Retrieve a paginated list of push notifications for the authenticated user. **Notification types:** - `job_update` - Job status changed - `bid_received` - New bid on your job - `bid_accepted` - Your bid was accepted - `message` - New message in conversation - `review` - New review or response - `token` - Token transaction completed - `referral` - Referral reward earned **Filtering:** - Filter by read/unread status - Filter by notification type - Sort by creation date (newest first) **Access control:** - Users see only their own notifications
Parameters
Name Type Description
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
unread_only
Query
boolean Show only unread notifications
type
Query
string Filter by notification type
Example Request
bash
curl -X GET https://api.probiddr.com/notifications \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/notifications', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/notifications',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Notifications retrieved successfully
json
{}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /notifications/unread-count
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Retrieve the count of unread notifications for the authenticated user. **Use cases:** - Display notification badge in UI - Check for new notifications without loading full list - Poll for updates (recommended: use WebSocket instead) **Performance:** - Lightweight endpoint optimized for frequent polling - Returns only the count, no notification details - Consider WebSocket for real-time updates
Example Request
bash
curl -X GET https://api.probiddr.com/notifications/unread-count \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/notifications/unread-count', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/notifications/unread-count',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Unread count retrieved successfully
json
{
  "unread_count": 5
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /notifications/preferences
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Retrieve notification preferences for the authenticated user. **Preference types:** - Push notifications (mobile/web) - In-app notifications - Notification types to receive **Notification categories:** - Job updates (status changes, new bids) - Messages (new messages, replies) - Reviews (new reviews, responses) - Tokens (transactions, low balance alerts) - Referrals (new referrals, rewards) - Platform updates (announcements, features) **Access control:** - Users can only view their own preferences
Example Request
bash
curl -X GET https://api.probiddr.com/notifications/preferences \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/notifications/preferences', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/notifications/preferences',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Notification preferences retrieved successfully
json
{
  "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "push_enabled": true,
  "in_app_enabled": true,
  "preferences": {
    "job_updates": true,
    "bid_received": true,
    "bid_accepted": true,
    "messages": true,
    "reviews": true,
    "token_transactions": false,
    "token_low_balance": true,
    "referral_rewards": true,
    "platform_updates": false
  },
  "updated_at": "2025-01-15T10:00:00Z"
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
PUT /notifications/preferences
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Update notification preferences for the authenticated user. **Configurable settings:** - Enable/disable push notifications globally - Enable/disable in-app notifications - Toggle specific notification types - Cannot disable critical notifications (security alerts) **Side effects:** - Updates user preferences - Emits `notification.preferences_updated` event - May unsubscribe from push notification channels **Important notes:** - Some notifications are mandatory (security, legal) - Changes take effect immediately
Request Body
Parameter Type Description
push_enabled boolean Enable push notifications
in_app_enabled boolean Enable in-app notifications
preferences object Notification type preferences
Example Request
bash
curl -X PUT https://api.probiddr.com/notifications/preferences \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "push_enabled": true,
    "in_app_enabled": true,
    "preferences": {
      "job_updates": true,
      "bid_received": true,
      "bid_accepted": true,
      "messages": true,
      "reviews": true,
      "token_transactions": false,
      "token_low_balance": true,
      "referral_rewards": true,
      "platform_updates": false
    }
  }'
javascript
const response = await fetch('https://api.probiddr.com/notifications/preferences', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "push_enabled": true,
    "in_app_enabled": true,
    "preferences": {
      "job_updates": true,
      "bid_received": true,
      "bid_accepted": true,
      "messages": true,
      "reviews": true,
      "token_transactions": false,
      "token_low_balance": true,
      "referral_rewards": true,
      "platform_updates": false
    }
  })
});

const data = await response.json();
python
import requests

response = requests.put(
    'https://api.probiddr.com/notifications/preferences',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "push_enabled": true,
      "in_app_enabled": true,
      "preferences": {
        "job_updates": true,
        "bid_received": true,
        "bid_accepted": true,
        "messages": true,
        "reviews": true,
        "token_transactions": false,
        "token_low_balance": true,
        "referral_rewards": true,
        "platform_updates": false
      }
    }
)

data = response.json()
Responses
200 OK
Preferences updated successfully
json
{
  "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "push_enabled": true,
  "in_app_enabled": true,
  "preferences": {
    "job_updates": true,
    "bid_received": true,
    "bid_accepted": true,
    "messages": true,
    "reviews": true,
    "token_transactions": false,
    "token_low_balance": true,
    "referral_rewards": true,
    "platform_updates": false
  },
  "updated_at": "2025-01-15T10:00:00Z"
}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
PUT /notifications/{id}/read
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Mark a specific notification as read. **Business rules:** - Can only mark your own notifications as read - Idempotent - safe to call multiple times - Sets read_at timestamp to current time **Side effects:** - Updates notification read status - Decrements unread count - Does not emit events (low-level operation) **Use cases:** - User clicks/taps notification - User views notification details - Auto-mark as read when displayed
Parameters
Name Type Description
id required
Path
string Notification ID
Example Request
bash
curl -X PUT https://api.probiddr.com/notifications/{id}/read \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/notifications/{id}/read', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.put(
    'https://api.probiddr.com/notifications/{id}/read',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Notification marked as read
json
{
  "id": "ntf_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user_id": "usr_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "type": "message",
  "title": "New message from John Smith",
  "body": "You have a new message about your job posting",
  "data": {
    "conversation_id": "cnv_3ADMbYYNYyyF5VHbDyG1UbsvJmC",
    "message_id": "msg_3ADMbZZNZzzF5VHbDyG1UbsvJmC"
  },
  "is_read": false,
  "read_at": "2025-01-20T15:00:00Z",
  "created_at": "2025-01-20T14:30:00Z"
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /notifications/mark-all-read
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Mark all notifications as read for the authenticated user. **Business rules:** - Marks all unread notifications as read - Sets read_at timestamp to current time - Idempotent - safe to call multiple times **Side effects:** - Updates all unread notifications - Resets unread count to zero - Does not emit individual events (bulk operation) **Use cases:** - "Mark all as read" button in UI - Clear notification badge - Bulk notification management **Performance:** - Optimized for bulk updates - May be throttled for users with many notifications
Example Request
bash
curl -X POST https://api.probiddr.com/notifications/mark-all-read \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/notifications/mark-all-read', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/notifications/mark-all-read',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
All notifications marked as read
json
{
  "message": "All notifications marked as read",
  "updated_count": 15
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
DELETE /notifications/{id}
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Delete a specific notification. **Business rules:** - Can only delete your own notifications - Permanent deletion (cannot be undone) - Does not affect related data (job, bid, message, etc.) **Side effects:** - Removes notification from database - Decrements unread count if notification was unread - Does not emit events (low-level operation) **Use cases:** - User dismisses notification - Clean up old notifications - Remove unwanted notifications
Parameters
Name Type Description
id required
Path
string Notification ID
Example Request
bash
curl -X DELETE https://api.probiddr.com/notifications/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/notifications/{id}', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.delete(
    'https://api.probiddr.com/notifications/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
204 No Content
Notification deleted successfully
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /reviews
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Retrieve a paginated list of reviews with optional filtering. **Filtering options:** - Filter by job_id to see all reviews for a specific job - Filter by contractor_id to see all reviews for a contractor - Filter by homeowner_id to see all reviews written by a homeowner - Filter by rating to see only high/low rated reviews **Access control:** - Public: Can view all published reviews - Reviews are only visible after job completion **Use cases:** - Display contractor's review history on profile - Show reviews for completed jobs - Browse platform-wide reviews
Parameters
Name Type Description
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
job_id
Query
string Filter by job ID
contractor_id
Query
string Filter by contractor ID
homeowner_id
Query
string Filter by homeowner ID (reviews written by)
min_rating
Query
integer Filter by minimum rating (1-5)
Example Request
bash
curl -X GET https://api.probiddr.com/reviews \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/reviews', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/reviews',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Reviews retrieved successfully
json
{}
400 Bad Request
Invalid request data or validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /reviews/{id}
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Retrieve detailed information about a specific review. **Review details include:** - Rating (1-5 stars) - Title and comment text - Contractor response (if provided) - Verification status - Report count (if flagged as inappropriate) **Access control:** - Public: Can view all published reviews
Parameters
Name Type Description
id required
Path
string Review ID
Example Request
bash
curl -X GET https://api.probiddr.com/reviews/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/reviews/{id}', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/reviews/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Review retrieved successfully
json
{
  "id": "rev_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "job_id": "job_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "contractor_id": "ctr_3ADMbYYNYyyF5VHbDyG1UbsvJmC",
  "homeowner_id": "hmw_3ADMbZZNZzzF5VHbDyG1UbsvJmC",
  "rating": 5,
  "title": "Excellent work!",
  "comment": "Professional, on time, and delivered great quality work.",
  "contractor_response": "Thank you for the kind words!",
  "is_verified": true,
  "created_at": "2025-01-20T10:00:00Z",
  "updated_at": "2025-01-20T15:00:00Z"
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /reviews/{id}
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Create a review for a completed job. **Business rules:** - Only homeowner can review the contractor - Job must be in "completed" status - Can only review each job once - Rating required (1-5 stars) - Comment must be at least 20 characters **Review verification:** - Reviews are verified if job payment was processed - Unverified reviews shown with disclaimer **Side effects:** - Creates review record - Updates contractor's average rating - Sends notification to contractor - Emits `review.created` event
Request Body
Parameter Type Description
job_id required string Job ID to review
rating required integer Rating (1-5 stars)
title string Review title (optional)
comment required string Review comment
Example Request
bash
curl -X POST https://api.probiddr.com/reviews/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "job_id": "job_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "rating": 5,
    "title": "Excellent work!",
    "comment": "Professional, on time, and delivered great quality work."
  }'
javascript
const response = await fetch('https://api.probiddr.com/reviews/{id}', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "job_id": "job_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
    "rating": 5,
    "title": "Excellent work!",
    "comment": "Professional, on time, and delivered great quality work."
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/reviews/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "job_id": "job_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
      "rating": 5,
      "title": "Excellent work!",
      "comment": "Professional, on time, and delivered great quality work."
    }
)

data = response.json()
Responses
201 Created
Review created successfully
json
{
  "id": "rev_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "job_id": "job_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "contractor_id": "ctr_3ADMbYYNYyyF5VHbDyG1UbsvJmC",
  "homeowner_id": "hmw_3ADMbZZNZzzF5VHbDyG1UbsvJmC",
  "rating": 5,
  "title": "Excellent work!",
  "comment": "Professional, on time, and delivered great quality work.",
  "contractor_response": "Thank you for the kind words!",
  "is_verified": true,
  "created_at": "2025-01-20T10:00:00Z",
  "updated_at": "2025-01-20T15:00:00Z"
}
400 Bad Request
Validation error or review not allowed
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
PUT /reviews/{id}
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Update a review that you created. **Business rules:** - Can only update your own reviews - Cannot update after 7 days of creation - Cannot update rating if contractor has already responded - Can only edit comment text and title **Side effects:** - Sets updated_at timestamp - Emits `review.updated` event - Notifies contractor of the update
Parameters
Name Type Description
id required
Path
string Review ID
Request Body
Parameter Type Description
rating integer Rating (1-5 stars)
title string Review title
comment string Review comment
Example Request
bash
curl -X PUT https://api.probiddr.com/reviews/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "rating": 5,
    "title": "Excellent work!",
    "comment": "Professional service and great quality."
  }'
javascript
const response = await fetch('https://api.probiddr.com/reviews/{id}', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "rating": 5,
    "title": "Excellent work!",
    "comment": "Professional service and great quality."
  })
});

const data = await response.json();
python
import requests

response = requests.put(
    'https://api.probiddr.com/reviews/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "rating": 5,
      "title": "Excellent work!",
      "comment": "Professional service and great quality."
    }
)

data = response.json()
Responses
200 OK
Review updated successfully
json
{
  "id": "rev_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "job_id": "job_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "contractor_id": "ctr_3ADMbYYNYyyF5VHbDyG1UbsvJmC",
  "homeowner_id": "hmw_3ADMbZZNZzzF5VHbDyG1UbsvJmC",
  "rating": 5,
  "title": "Excellent work!",
  "comment": "Professional, on time, and delivered great quality work.",
  "contractor_response": "Thank you for the kind words!",
  "is_verified": true,
  "created_at": "2025-01-20T10:00:00Z",
  "updated_at": "2025-01-20T15:00:00Z"
}
400 Bad Request
Validation error or update not allowed
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
DELETE /reviews/{id}
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Delete a review that you created. **Business rules:** - Can only delete your own reviews - Cannot delete after 7 days of creation - Cannot delete if contractor has responded (must contact support) - Soft delete - review is hidden but retained for records **Side effects:** - Marks review as deleted (soft delete) - Updates contractor's average rating - Emits `review.deleted` event
Parameters
Name Type Description
id required
Path
string Review ID
Example Request
bash
curl -X DELETE https://api.probiddr.com/reviews/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/reviews/{id}', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.delete(
    'https://api.probiddr.com/reviews/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
204 No Content
Review deleted successfully
400 Bad Request
Deletion not allowed
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /reviews/{id}/response
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Allow contractor to respond to a review. **Business rules:** - Only the reviewed contractor can respond - Can only respond once per review - Response must be professional and constructive - Maximum 1000 characters **Response guidelines:** - Be professional and courteous - Address concerns raised in review - Thank reviewer for feedback - Inappropriate responses can be reported **Side effects:** - Adds contractor response to review - Notifies homeowner of response - Emits `review.response_added` event
Parameters
Name Type Description
id required
Path
string Review ID
Request Body
Parameter Type Description
response required string Contractor's response to the review
Example Request
bash
curl -X POST https://api.probiddr.com/reviews/{id}/response \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "response": "Thank you for the kind words! It was a pleasure working with you and I'm glad you're happy with the results."
  }'
javascript
const response = await fetch('https://api.probiddr.com/reviews/{id}/response', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "response": "Thank you for the kind words! It was a pleasure working with you and I'm glad you're happy with the results."
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/reviews/{id}/response',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "response": "Thank you for the kind words! It was a pleasure working with you and I'm glad you're happy with the results."
    }
)

data = response.json()
Responses
200 OK
Response added successfully
json
{
  "id": "rev_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "job_id": "job_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "contractor_id": "ctr_3ADMbYYNYyyF5VHbDyG1UbsvJmC",
  "homeowner_id": "hmw_3ADMbZZNZzzF5VHbDyG1UbsvJmC",
  "rating": 5,
  "title": "Excellent work!",
  "comment": "Professional, on time, and delivered great quality work.",
  "contractor_response": "Thank you for the kind words!",
  "is_verified": true,
  "created_at": "2025-01-20T10:00:00Z",
  "updated_at": "2025-01-20T15:00:00Z"
}
400 Bad Request
Validation error or response not allowed
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /reviews/{id}/report
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Report a review or contractor response as inappropriate. **Report reasons:** - Offensive language or harassment - Fake or fraudulent review - Contains personal information - Spam or irrelevant content - Other (requires explanation) **Business rules:** - Any authenticated user can report - Cannot report same review multiple times - Reports are reviewed by moderation team **Side effects:** - Creates report record - Flags review for moderation - Emits `review.reported` event - Sends notification to moderation team
Parameters
Name Type Description
id required
Path
string Review ID
Request Body
Parameter Type Description
reason required string Reason for reporting
details string Additional details (required if reason is "other")
Example Request
bash
curl -X POST https://api.probiddr.com/reviews/{id}/report \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "offensive",
    "details": "This review contains offensive language and personal attacks."
  }'
javascript
const response = await fetch('https://api.probiddr.com/reviews/{id}/report', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "reason": "offensive",
    "details": "This review contains offensive language and personal attacks."
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/reviews/{id}/report',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "reason": "offensive",
      "details": "This review contains offensive language and personal attacks."
    }
)

data = response.json()
Responses
200 OK
Review reported successfully
json
{
  "message": "Review reported successfully. Our team will review it shortly."
}
400 Bad Request
Validation error or already reported
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /search/jobs
Search for jobs with advanced filtering and sorting options. **Access control:** - Homeowners: See only their own jobs (all statuses) - Contractors: See only open jobs - Admin: See all jobs **Search behavior:** - Full-text search on title and description - Results ranked by relevance - Supports partial word matching **Filters:** - Location: city, state, zip_code (radius search coming soon) - Category: specific job category - Budget: min/max range - Urgency: standard, priority, urgent - Date: posted within last N days **Sorting:** - relevance (default) - date (newest first) - budget_max (highest first) - urgency (urgent β†’ priority β†’ standard)
Parameters
Name Type Description
q required
Query
string Search query (searches title and description)
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
category
Query
string Filter by job category
urgency
Query
string Filter by urgency level
budget_min
Query
number Minimum budget (greater than or equal)
budget_max
Query
number Maximum budget (less than or equal)
city
Query
string Filter by city
state
Query
string Filter by state
zip_code
Query
string Filter by zip code
posted_within_days
Query
integer Posted within last N days
sort
Query
string Sort order
Example Request
bash
curl -X GET https://api.probiddr.com/search/jobs \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/search/jobs', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/search/jobs',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Jobs matching search criteria
json
{}
400 Bad Request
Invalid search parameters
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /search/contractors
Search for contractors with filtering options. **Access control:** - Public endpoint (no authentication required) - Returns only verified contractors **Search behavior:** - Full-text search on company_name, bio, and specialties - Results ranked by relevance - Supports partial word matching **Filters:** - Service area: city or region - Specialties: specific categories - Years in business: minimum experience - License state: where licensed **Sorting:** - relevance (default) - years_in_business (most experienced first) - jobs_completed (most active first - requires stats)
Parameters
Name Type Description
q required
Query
string Search query (searches company name, bio, specialties)
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
service_area
Query
string Filter by service area (city or region)
specialty
Query
string Filter by specialty/category
min_years
Query
integer Minimum years in business
license_state
Query
string Filter by license state
sort
Query
string Sort order
Example Request
bash
curl -X GET https://api.probiddr.com/search/contractors \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/search/contractors', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/search/contractors',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Contractors matching search criteria
json
{}
400 Bad Request
Invalid search parameters
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /search/saved
⚠️ Coming Soon - Retrieve user's saved searches. **This endpoint is a stub and not yet implemented.** **Planned features:** - Save frequent searches for quick access - Name and organize saved searches - Set up alerts for new results matching saved searches - Track last run date and new results count **Use cases:** - Contractors save searches for job types in their area - Homeowners save searches for specific contractor specialties
Parameters
Name Type Description
page
Query
integer Page number for pagination (1-indexed)
per_page
Query
integer Number of items per page
Example Request
bash
curl -X GET https://api.probiddr.com/search/saved \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/search/saved', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/search/saved',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
501
Not implemented yet
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /search/saved
⚠️ Coming Soon - Save a search for quick access and alerts. **This endpoint is a stub and not yet implemented.** **Planned features:** - Save current search query and filters - Give search a custom name - Enable/disable alerts for new results - Set alert frequency (instant, daily, weekly) **Request will include:** - Search query and filters - Custom name for saved search - Alert preferences
Request Body
Parameter Type Description
name required string Custom name for this saved search
search_type required string Type of search
query required string Search query
filters object Search filters (as used in search endpoint)
enable_alerts boolean Enable alerts for new results
alert_frequency string How often to send alerts
Example Request
bash
curl -X POST https://api.probiddr.com/search/saved \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Kitchen Jobs in SF",
    "search_type": "jobs",
    "query": "kitchen remodel",
    "filters": {
      "city": "San Francisco",
      "category": "Remodeling"
    },
    "enable_alerts": true,
    "alert_frequency": "daily"
  }'
javascript
const response = await fetch('https://api.probiddr.com/search/saved', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "name": "Kitchen Jobs in SF",
    "search_type": "jobs",
    "query": "kitchen remodel",
    "filters": {
      "city": "San Francisco",
      "category": "Remodeling"
    },
    "enable_alerts": true,
    "alert_frequency": "daily"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/search/saved',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "name": "Kitchen Jobs in SF",
      "search_type": "jobs",
      "query": "kitchen remodel",
      "filters": {
        "city": "San Francisco",
        "category": "Remodeling"
      },
      "enable_alerts": true,
      "alert_frequency": "daily"
    }
)

data = response.json()
Responses
501
Not implemented yet
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
POST /uploads
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Initiate a file upload to S3 storage. **Upload flow:** 1. Client calls this endpoint to initiate upload 2. Server creates upload record and returns upload_id 3. Client calls GET /uploads/:id/presigned to get S3 URL 4. Client uploads file directly to S3 using presigned URL 5. Client confirms upload completion (optional) **File type restrictions:** - Images: jpg, jpeg, png, gif, webp (max 10MB) - Documents: pdf (max 20MB) - No executable files allowed **Business rules:** - User must be authenticated - Upload record expires after 1 hour if not completed - Files are scanned for viruses before being marked as available **Side effects:** - Creates upload record in database - Generates unique upload_id (upl_xxx) - Emits `upload.initiated` event **Access control:** - Users can only initiate uploads for themselves
Request Body
Parameter Type Description
file_name required string Original file name
file_size required integer File size in bytes (max 20MB)
mime_type required string File MIME type
context required string Upload context (where file will be used)
context_id string ID of related entity (job_id, bid_id, etc.) - optional during initiation
Example Request
bash
curl -X POST https://api.probiddr.com/uploads \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "file_name": "kitchen-before.jpg",
    "file_size": 2048576,
    "mime_type": "image/jpeg",
    "context": "job_attachment",
    "context_id": "job_3ADMbXXNXxxF5VHbDyG1UbsvJmC"
  }'
javascript
const response = await fetch('https://api.probiddr.com/uploads', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  },
  body: JSON.stringify({
    "file_name": "kitchen-before.jpg",
    "file_size": 2048576,
    "mime_type": "image/jpeg",
    "context": "job_attachment",
    "context_id": "job_3ADMbXXNXxxF5VHbDyG1UbsvJmC"
  })
});

const data = await response.json();
python
import requests

response = requests.post(
    'https://api.probiddr.com/uploads',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    },
    json={
      "file_name": "kitchen-before.jpg",
      "file_size": 2048576,
      "mime_type": "image/jpeg",
      "context": "job_attachment",
      "context_id": "job_3ADMbXXNXxxF5VHbDyG1UbsvJmC"
    }
)

data = response.json()
Responses
201 Created
Upload initiated successfully
json
{
  "id": "upl_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user_id": "usr_3ADMbYYNYyyF5VHbDyG1UbsvJmC",
  "file_name": "kitchen-before.jpg",
  "file_size": 2048576,
  "mime_type": "image/jpeg",
  "context": "job_attachment",
  "context_id": "job_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "status": "completed",
  "s3_key": "uploads/2025/01/20/upl_3ADMbXXNXxxF5VHbDyG1UbsvJmC/kitchen-before.jpg",
  "s3_url": "https://cdn.probiddr.com/uploads/2025/01/20/upl_3ADMbXXNXxxF5VHbDyG1UbsvJmC/kitchen-before.jpg",
  "expires_at": "2025-01-20T15:30:00Z",
  "created_at": "2025-01-20T14:30:00Z",
  "updated_at": "2025-01-20T14:32:00Z"
}
400 Bad Request
Validation error
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /uploads/{id}
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Retrieve details about a specific upload. **Upload statuses:** - `pending` - Upload initiated, awaiting S3 upload - `uploading` - File is being uploaded to S3 - `processing` - File uploaded, being scanned/processed - `completed` - Upload complete and file is available - `failed` - Upload failed (virus detected, processing error, etc.) - `expired` - Upload record expired before completion **Access control:** - Users can only view their own uploads - Or uploads attached to resources they have access to **Use cases:** - Check upload status - Get file URL after upload completes - Verify upload before attaching to job/bid
Parameters
Name Type Description
id required
Path
string Upload ID
Example Request
bash
curl -X GET https://api.probiddr.com/uploads/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/uploads/{id}', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/uploads/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Upload details retrieved successfully
json
{
  "id": "upl_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "user_id": "usr_3ADMbYYNYyyF5VHbDyG1UbsvJmC",
  "file_name": "kitchen-before.jpg",
  "file_size": 2048576,
  "mime_type": "image/jpeg",
  "context": "job_attachment",
  "context_id": "job_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "status": "completed",
  "s3_key": "uploads/2025/01/20/upl_3ADMbXXNXxxF5VHbDyG1UbsvJmC/kitchen-before.jpg",
  "s3_url": "https://cdn.probiddr.com/uploads/2025/01/20/upl_3ADMbXXNXxxF5VHbDyG1UbsvJmC/kitchen-before.jpg",
  "expires_at": "2025-01-20T15:30:00Z",
  "created_at": "2025-01-20T14:30:00Z",
  "updated_at": "2025-01-20T14:32:00Z"
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
DELETE /uploads/{id}
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Delete an upload and its associated file from S3. **Business rules:** - Can only delete your own uploads - Cannot delete if upload is attached to a published resource (job, bid, etc.) - Can delete pending/failed uploads at any time - Soft delete - file is marked for deletion but retained for 30 days **Side effects:** - Marks upload as deleted - Queues file for deletion from S3 (async) - Emits `upload.deleted` event - Removes upload from any pending associations **Access control:** - Users can only delete their own uploads - Admins can delete any upload **Use cases:** - Remove unwanted uploads - Clean up failed uploads - Cancel pending upload
Parameters
Name Type Description
id required
Path
string Upload ID
Example Request
bash
curl -X DELETE https://api.probiddr.com/uploads/{id} \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/uploads/{id}', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.delete(
    'https://api.probiddr.com/uploads/{id}',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
204 No Content
Upload deleted successfully
400 Bad Request
Cannot delete upload
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
GET /uploads/{id}/presigned
⚠️ **Coming Soon - Not Yet Implemented** This endpoint is planned but not yet implemented. The API will return 501 Not Implemented. Get a presigned S3 URL to upload file directly to S3. **Upload flow:** 1. Client initiates upload via POST /uploads 2. Client calls this endpoint to get presigned URL 3. Client uploads file directly to S3 using presigned URL (PUT request) 4. Client optionally confirms upload completion **Presigned URL details:** - Valid for 15 minutes - Allows PUT request only - Includes required headers (Content-Type, etc.) - Uploads directly to S3 (does not go through API) **After uploading:** - File is automatically scanned for viruses - Upload status changes to "processing" then "completed" - Client can poll GET /uploads/:id to check status **Security:** - URL expires after 15 minutes - URL is single-use - File size validated server-side - MIME type validated server-side **Access control:** - Can only get presigned URL for your own uploads
Parameters
Name Type Description
id required
Path
string Upload ID
Example Request
bash
curl -X GET https://api.probiddr.com/uploads/{id}/presigned \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
javascript
const response = await fetch('https://api.probiddr.com/uploads/{id}/presigned', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
});

const data = await response.json();
python
import requests

response = requests.get(
    'https://api.probiddr.com/uploads/{id}/presigned',
    headers={
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
    }
)

data = response.json()
Responses
200 OK
Presigned URL generated successfully
json
{
  "upload_id": "upl_3ADMbXXNXxxF5VHbDyG1UbsvJmC",
  "presigned_url": "https://s3.amazonaws.com/probiddr-uploads/uploads/2025/01/20/upl_3ADMbXXNXxxF5VHbDyG1UbsvJmC/kitchen-before.jpg?AWSAccessKeyId=AKIAI44QH8DHBEXAMPLE&Expires=1737388800&Signature=abcd1234...",
  "expires_at": "2025-01-20T14:45:00Z",
  "headers": {
    "Content-Type": "image/jpeg",
    "Content-Length": "2048576"
  },
  "method": "PUT"
}
400 Bad Request
Cannot generate presigned URL
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
401 Unauthorized
Authentication required or invalid token
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
403 Forbidden
Insufficient permissions to perform action
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
404 Not Found
Resource not found
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
429
Rate limit exceeded
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
501
Not implemented
json
{
  "status": 400,
  "error": "Validation failed",
  "details": {},
  "blocked_fields": [
    "string"
  ]
}
Authentication
Secure JWT-based authentication for homeowners and contractors. All API requests require a valid access token.
POST
/auth/login
User login
POST
/auth/signup/homeowner
Homeowner signup
POST
/auth/signup/contractor
Contractor signup
POST
/auth/logout
User logout
POST
/auth/refresh
Refresh JWT token
Jobs API
Create, browse, and manage job postings. Homeowners post jobs, contractors search and bid on them.
GET
/jobs
List jobs
POST
/jobs
Create job
GET
/jobs/{id}
Get job details
PUT
/jobs/{id}
Update job
DELETE
/jobs/{id}
Delete job
GET
/jobs/{id}/bids
Get bids for job
POST
/jobs/{id}/publish
Publish job (draft β†’ open)
POST
/jobs/{id}/award
Award job to bid (open β†’ awarded)
POST
/jobs/{id}/checkin
Check-in to job (awarded β†’ in_progress)
POST
/jobs/{id}/submit-for-review
Submit work for review (in_progress β†’ in_review)
POST
/jobs/{id}/approve
Approve completed work (in_review β†’ completed)
POST
/jobs/{id}/dispute
Dispute completed work (in_review β†’ disputed)
POST
/jobs/{id}/escalate
Escalate dispute to admin (disputed β†’ escalated)
POST
/jobs/{id}/reach-decision
Admin reaches decision (escalated β†’ decision_reached)
POST
/jobs/{id}/file-appeal
File appeal against decision (decision_reached β†’ appealed)
POST
/jobs/{id}/finalize-appeal
Finalize appeal (appealed β†’ completed)
POST
/jobs/{id}/resolve-dispute
Resolve dispute directly (disputed β†’ completed)
POST
/jobs/{id}/complete
Force complete job (any β†’ completed)
POST
/jobs/{id}/cancel
Cancel job (any β†’ cancelled)
Homeowners API
Manage homeowner profiles, job postings, and bid responses.
GET
/homeowners
List homeowners
POST
/homeowners
Create homeowner profile
GET
/homeowner/user/{user_id}
Get homeowner by user_id
GET
/homeowners/{id}
Get homeowner profile
PUT
/homeowners/{id}
Update homeowner profile
DELETE
/homeowners/{id}
Delete homeowner profile
GET
/homeowners/{id}/jobs
Get homeowner's jobs
GET
/homeowners/{id}/reviews
Get homeowner reviews
GET
/homeowners/{id}/stats
Get homeowner statistics
Contractors API
Manage contractor profiles, licenses, portfolios, and bids.
GET
/contractors
List contractors
POST
/contractors
Create contractor profile
GET
/contractors/{id}
Get contractor profile
PUT
/contractors/{id}
Update contractor profile
DELETE
/contractors/{id}
Delete contractor profile
GET
/contractors/{id}/bids
Get contractor's bids
GET
/contractors/{id}/reviews
Get contractor reviews
GET
/contractors/{id}/stats
Get contractor statistics
Bids API
Submit, manage, and respond to bids on job postings.
GET
/bids
List bids
POST
/bids
Create bid
GET
/bids/{id}
Get bid details
PUT
/bids/{id}
Update bid
DELETE
/bids/{id}
Withdraw bid
POST
/bids/{id}/submit
Submit bid (draft β†’ pending)
POST
/bids/{id}/accept
Accept bid (pending/viewed/shortlisted β†’ accepted)
POST
/bids/{id}/reject
Reject bid (pending/viewed/shortlisted β†’ rejected)
Messages API
Send and receive messages between homeowners and contractors.
GET
/conversations
List conversations
GET
/conversations/{id}
Get conversation details
GET
/conversations/{id}/messages
Get messages in conversation
POST
/conversations/{id}/messages
Send message
GET
/conversations/bid/{bid_id}
Get conversation by bid
GET
/messages/unread-count
Get unread message count
POST
/messages
Create direct message
POST
/messages/mark-read
Bulk mark messages as read
PUT
/messages/{id}/read
Mark single message as read
DELETE
/messages/{id}
Delete message
Tokens API
Manage virtual currency. Tokens are required to post jobs, submit bids, and access premium features.
GET
/tokens/balance
Get token balance
GET
/tokens/transactions
Get transaction history
GET
/tokens/packages
Get token packages
GET
/tokens/holds
Get active holds
POST
/tokens/holds
Create hold
POST
/tokens/purchase
Purchase token package
POST
/tokens/credit
Credit tokens (admin only)
POST
/tokens/debit
Debit tokens (admin only)
POST
/tokens/holds/{id}/capture
Capture hold (admin only)
POST
/tokens/holds/{id}/release
Release hold (admin only)
Referrals API
Track referrals and earn token rewards for inviting friends.
GET
/referrals/codes
Get user's referral codes
POST
/referrals/codes
Create referral code
GET
/referrals/stats
Get referral statistics
GET
/referrals/rewards
Get referral rewards history
POST
/referrals/apply
Apply referral code
Sessions API
Manage user sessions and authentication tokens.
GET
/sessions
List active sessions
DELETE
/sessions/{token_id}
Revoke session
Subscriptions API
Manage email notification preferences and subscriptions.
GET
/subscriptions
Get subscription preferences
PUT
/subscriptions
Update subscription preferences
GET
/subscriptions/options/{type}
Get available subscription options
POST
/subscriptions/initialize
Initialize default preferences
POST
/subscriptions/unsubscribe
Unsubscribe via email link
Notifications API
Receive and manage real-time notifications for jobs, bids, and messages.
GET
/notifications
List notifications
GET
/notifications/unread-count
Get unread notification count
GET
/notifications/preferences
Get notification preferences
PUT
/notifications/preferences
Update notification preferences
PUT
/notifications/{id}/read
Mark notification as read
POST
/notifications/mark-all-read
Mark all notifications as read
DELETE
/notifications/{id}
Delete notification
Reviews API
Submit and view reviews for contractors and completed jobs.
GET
/reviews
List reviews with filters
GET
/reviews/{id}
Get review details
POST
/reviews/{id}
Create review for completed job
PUT
/reviews/{id}
Update own review
DELETE
/reviews/{id}
Delete own review
POST
/reviews/{id}/response
Add contractor response to review
POST
/reviews/{id}/report
Report inappropriate review
Uploads API
Upload and manage files, images, and documents.
POST
/uploads
Initiate file upload
GET
/uploads/{id}
Get upload details
DELETE
/uploads/{id}
Delete upload
GET
/uploads/{id}/presigned
Get presigned S3 URL for upload