# TAB Affiliates API - Reference

Complete reference for the TAB Affiliates API used in the racing scraper project.

---

## Base Configuration

**Base URL**: `https://api.tab.com.au/affiliates/v1/racing/`  
**Authentication**: API Key (if required)  
**Rate Limits**: Unknown - using conservative 100/min  
**Timeout**: 10 seconds

---

## Endpoints

### GET /meetings

Fetch list of meetings for a given date, country, and category.

**Parameters:**
- `date_from` (string, required): Start date in YYYY-MM-DD format or "today"
- `date_to` (string, required): End date in YYYY-MM-DD format or "today"
- `country` (string): "AUS" or "NZ"
- `category` (string): "T" (Thoroughbred) or "H" (Harness)
- `enc` (string): "json" (response encoding)

**Example Request:**
```http
GET /affiliates/v1/racing/meetings?date_from=today&date_to=today&country=AUS&category=T&enc=json
```

**Example Response:**
```json
{
  "header": {
    "title": "Race Meetings",
    "generated_time": "2026-01-13T09:11:46.722887528Z",
    "url": "/affiliates/v1/racing/meetings?..."
  },
  "params": {
    "enc": "json",
    "date_from": "2026-01-13T00:00:00Z",
    "date_to": "2026-01-13T00:00:00Z",
    "type": "T",
    "country": "AUS"
  },
  "data": {
    "meetings": [
      {
        "meeting": "6e2182c6-9cf7-41dc-b0e0-5af4e2bd67ef",
        "name": "Yarra Valley",
        "date": "2026-01-13T00:00:00Z",
        "track_condition": "Good",
        "category": "T",
        "category_name": "Thoroughbred Horse Racing",
        "country": "AUS",
        "state": "VIC",
        "races": [
          {
            "id": "cb9d4f39-c7ce-4778-a0be-f3899d86f1bd",
            "race_number": 1,
            "name": "Bet365 Top Finishes Maiden Plate",
            "start_time": "2026-01-13T02:30:00Z",
            "distance": 1000,
            "status": "Final"
          }
        ]
      }
    ]
  }
}
```

**Response Fields:**
- `meeting` (uuid): Unique meeting identifier
- `name` (string): Track/venue name
- `date` (datetime): Meeting date
- `track_condition` (string): Track condition (e.g., "Good", "Heavy")
- `category` (string): "T" or "H"
- `country` (string): "AUS" or "NZ"
- `state` (string): State/region code
- `races` (array): Nested race objects (basic info only)

---

### GET /meetings/:id

Fetch detailed information for a specific meeting including all races.

**Parameters:**
- `id` (uuid, required): Meeting UUID
- `enc` (string): "json"

**Example Request:**
```http
GET /affiliates/v1/racing/meetings/6e2182c6-9cf7-41dc-b0e0-5af4e2bd67ef?enc=json
```

**Example Response:**
```json
{
  "header": {
    "title": "Get meeting by id",
    "generated_time": "2026-01-13T09:12:36.571668056Z"
  },
  "data": {
    "meetings": [
      {
        "meeting": "6e2182c6-9cf7-41dc-b0e0-5af4e2bd67ef",
        "name": "Yarra Valley",
        "date": "2026-01-13T00:00:00Z",
        "track_condition": "Good",
        "category": "T",
        "country": "AUS",
        "state": "VIC",
        "weather": "OCAST",
        "races": [
          {
            "id": "cb9d4f39-c7ce-4778-a0be-f3899d86f1bd",
            "race_number": 1,
            "name": "Bet365 Top Finishes Maiden Plate",
            "start_time": "2026-01-13T02:30:00Z",
            "tote_start_time": "15:30:00",
            "track_condition": "Good",
            "distance": 1000,
            "weather": "OCAST",
            "status": "Final"
          }
        ],
        "video_channels": ["Trackside1", "Trackside2"],
        "quaddie": [5, 6, 7, 8],
        "early_quaddie": [1, 2, 3, 4],
        "tote_meeting_number": 22
      }
    ]
  }
}
```

**Additional Fields (vs /meetings list):**
- `weather` (string): Weather condition
- `video_channels` (array): Streaming channels
- `quaddie` (array): Quaddie race numbers
- `early_quaddie` (array): Early quaddie race numbers
- `tote_meeting_number` (integer): Tote reference

---

### GET /races/:id

Fetch detailed race information including full runners array.

**Parameters:**
- `id` (uuid, required): Race UUID
- `enc` (string): "json"

**Example Request:**
```http
GET /affiliates/v1/racing/races/cb9d4f39-c7ce-4778-a0be-f3899d86f1bd?enc=json
```

**Example Response Structure:**
```json
{
  "header": {
    "title": "Get race by id",
    "generated_time": "2026-01-13T09:15:00.000000000Z"
  },
  "data": {
    "races": [
      {
        "id": "cb9d4f39-c7ce-4778-a0be-f3899d86f1bd",
        "race_number": 1,
        "name": "Bet365 Top Finishes Maiden Plate",
        "start_time": "2026-01-13T02:30:00Z",
        "distance": 1000,
        "track_condition": "Good",
        "status": "Final",
        "runners": [
          {
            "runner_number": 1,
            "horse_name": "Example Horse",
            "barrier": 3,
            "weight": 58.5,
            "jockey_name": "J. Smith",
            "trainer_name": "T. Jones",
            "form": "1-2-3-4",
            "scratched": false,
            "fixed_odds": {
              "win": 5.50,
              "place": 2.20
            }
          }
        ],
        "results": [
          {
            "runner_number": 1,
            "finish_position": 1,
            "margin": 0.0,
            "time": 58.42
          }
        ]
      }
    ]
  }
}
```

**Runner Fields:**
- `runner_number` (integer): Runner number
- `horse_name` (string): Horse name
- `horse_id` (uuid, optional): Horse identifier
- `barrier` (integer): Barrier position
- `weight` (number): Carrying weight (kg)
- `jockey_name` (string): Jockey name
- `jockey_id` (uuid, optional): Jockey identifier
- `trainer_name` (string): Trainer name
- `trainer_id` (uuid, optional): Trainer identifier
- `form` (string): Recent form (e.g., "1-2-3-4-5")
- `scratched` (boolean): Whether scratched
- `fixed_odds` (object, optional): Current odds

**Results Fields (after race):**
- `runner_number` (integer): Links to runner
- `finish_position` (integer): Final position (1 = winner)
- `margin` (number): Distance behind winner (lengths)
- `time` (number): Race time in seconds
- `dividends` (object, optional): Win/place payouts

---

## Response Patterns

### Common Response Structure

All endpoints follow this pattern:

```json
{
  "header": {
    "title": "Endpoint Description",
    "generated_time": "ISO-8601 timestamp",
    "url": "Request URL"
  },
  "params": {
    // Echo of request parameters
  },
  "data": {
    // Actual response data
  }
}
```

### Date/Time Handling

- All dates/times in UTC
- ISO 8601 format: `2026-01-13T02:30:00Z`
- `date_from`/`date_to` can be:
  - "today" (relative)
  - "YYYY-MM-DD" (absolute)
  - ISO 8601 datetime

### Status Values

**Race Status:**
- `"Upcoming"` - Not yet run
- `"Final"` - Complete, official results
- `"Interim"` - Complete, unofficial
- `"Abandoned"` - Cancelled

**Track Conditions:**
- `"Good"`, `"Good4"` - Ideal
- `"Soft5"`, `"Soft6"`, `"Soft7"` - Wet
- `"Heavy8"`, `"Heavy9"`, `"Heavy10"` - Very wet

---

## Error Responses

### 400 Bad Request

```json
{
  "error": "Invalid parameter",
  "message": "date_from must be in YYYY-MM-DD format"
}
```

### 404 Not Found

```json
{
  "error": "Not found",
  "message": "Meeting with id 'xxx' not found"
}
```

### 429 Too Many Requests

```json
{
  "error": "Rate limit exceeded",
  "retry_after": 60
}
```

Headers:
- `Retry-After: 60` (seconds)

### 500 Server Error

```json
{
  "error": "Internal server error",
  "message": "An unexpected error occurred"
}
```

---

## Rate Limiting

**Official Limits**: Unknown  
**Our Strategy**: 100 requests/minute with reservoir refill

**Handling:**
- Use bottleneck library for rate limiting
- Exponential backoff on 429 responses
- Track `X-RateLimit-*` headers if present

---

## Best Practices

### Efficient Data Collection

1. **Morning Scrape**:
   ```
   GET /meetings?date_from=today&date_to=today&country=AUS&category=T
   → Returns meetings with basic race info
   
   For each meeting:
     GET /meetings/{meeting_id}
     → Returns full meeting details + races
   
   For each race:
     GET /races/{race_id}
     → Returns full runners array
   ```

2. **Pre-Race Updates**:
   ```
   GET /races/{race_id}
   → Check for scratches, condition changes
   → Compare with previous scrape
   ```

3. **Post-Race Results**:
   ```
   GET /races/{race_id}
   → Fetch results array
   → Verify status is "Final"
   ```

### Optimization

- **Batch**: Minimize API calls by using nested data when available
- **Cache**: Store meeting list, only re-fetch when needed
- **Parallel**: Fetch multiple races concurrently (respect rate limits)
- **Conditional**: Only fetch full race data when needed

### Validation

Always validate responses with Zod:

```typescript
const meetingsSchema = z.object({
  header: z.object({
    generated_time: z.string(),
  }),
  data: z.object({
    meetings: z.array(meetingSchema),
  }),
});

const validated = meetingsSchema.parse(response.data);
```

---

## TypeScript Types

### Generated from OpenAPI

See `openapi.json` for complete specification.

**Key Types:**

```typescript
interface Meeting {
  meeting: string; // UUID
  name: string;
  date: string; // ISO 8601
  track_condition: string | null;
  category: 'T' | 'H';
  country: 'AUS' | 'NZ';
  state: string;
  races: Race[];
}

interface Race {
  id: string; // UUID
  race_number: number;
  name: string;
  start_time: string; // ISO 8601
  distance: number;
  track_condition: string | null;
  status: string;
  runners?: Runner[];
  results?: Result[];
}

interface Runner {
  runner_number: number;
  horse_name: string;
  horse_id?: string;
  barrier: number;
  weight: number;
  jockey_name: string;
  jockey_id?: string;
  trainer_name: string;
  trainer_id?: string;
  form?: string;
  scratched: boolean;
  fixed_odds?: {
    win: number;
    place: number;
  };
}

interface Result {
  runner_number: number;
  finish_position: number;
  margin: number;
  time: number;
  dividends?: {
    win: number;
    place: number;
  };
}
```

---

## Testing

### Mock Responses

Use example JSON files for testing:
- `list-of-meetings.json` - GET /meetings response
- `specified-meeting.json` - GET /meetings/:id response
- `specified-race.json` - GET /races/:id response (if available)

### Integration Testing

Test against real API with:
- Test account credentials
- Small date range (single day)
- Rate limiting enabled
- Proper error handling

---

## Troubleshooting

### Common Issues

**Issue**: 429 Rate Limit  
**Solution**: Reduce request rate, implement backoff

**Issue**: Empty results  
**Solution**: Check date format, verify meetings exist for date

**Issue**: Stale data  
**Solution**: API may cache, use `generated_time` to check freshness

**Issue**: Missing runners  
**Solution**: Use GET /races/:id not /meetings (nested races lack runners)

---

## Change Log

**Known Changes:**
- API structure stable since project start
- Monitor for schema changes
- Watch for new fields or deprecated fields

**Update Strategy:**
- Regenerate types from openapi.json when spec updates
- Use Zod validation to catch unexpected changes
- Log warnings for unknown fields

---

**Reference Files:**
- `openapi.json` - Complete OpenAPI specification
- `list-of-meetings.json` - Example meetings list response
- `specified-meeting.json` - Example meeting detail response
