The PodcastIndex API uses SHA1 hash-based authentication with three headers:
X-Auth-Key: YOUR_API_KEY
X-Auth-Date: UNIX_TIMESTAMP
Authorization: SHA1_HASH
- Get current Unix timestamp
- Concatenate:
apiKey + apiSecret + timestamp - Generate SHA1 hash of the concatenated string
- Send all three values in headers
// In NetworkService.swift
let authTime = String(Int(Date().timeIntervalSince1970))
let authHash = generateAuthHash(authTime: authTime)
request.setValue(APIConstants.apiKey, forHTTPHeaderField: "X-Auth-Key")
request.setValue(authTime, forHTTPHeaderField: "X-Auth-Date")
request.setValue(authHash, forHTTPHeaderField: "Authorization")Endpoint: /search/byterm
Purpose: Search for podcasts by keyword
Parameters:
q(required): Search term
Example Request:
GET https://api.podcastindex.org/api/1.0/search/byterm?q=tech
Response:
{
"status": "true",
"feeds": [
{
"id": 920666,
"title": "Tech News Today",
"author": "TWiT",
"description": "The most important tech news...",
"artwork": "https://example.com/artwork.jpg",
"url": "https://example.com/feed.xml",
"categories": {
"1": "Technology",
"2": "News"
}
}
]
}Used In: SearchPodcastsUseCase
Endpoint: /podcasts/trending
Purpose: Get currently trending podcasts
Parameters:
max(optional): Maximum number of results (default: 10)
Example Request:
GET https://api.podcastindex.org/api/1.0/podcasts/trending?max=20
Response: Same structure as search
Used In: GetTrendingPodcastsUseCase
Endpoint: /podcasts/byfeedid
Purpose: Get detailed information about a specific podcast
Parameters:
id(required): Feed ID
Example Request:
GET https://api.podcastindex.org/api/1.0/podcasts/byfeedid?id=920666
Response: Same structure as search (single feed)
Used In: PodcastRepository.getPodcastById()
Endpoint: /episodes/byfeedid
Purpose: Get all episodes for a specific podcast
Parameters:
id(required): Feed IDmax(optional): Maximum number of results (default: 10)
Example Request:
GET https://api.podcastindex.org/api/1.0/episodes/byfeedid?id=920666&max=50
Response:
{
"status": "true",
"items": [
{
"id": 123456789,
"title": "Episode 245: AI Revolution",
"description": "We discuss the latest in AI...",
"enclosureUrl": "https://example.com/episode.mp3",
"duration": 2730,
"datePublished": 1716163200,
"image": "https://example.com/episode-art.jpg"
}
]
}Used In: GetEpisodesUseCase
struct PodcastDTO: Codable {
let id: Int
let title: String
let author: String?
let description: String?
let artwork: String?
let url: String
let categories: [String: String]?
func toDomain() -> Podcast {
Podcast(
id: id,
title: title,
author: author ?? "Unknown",
description: description,
artworkURL: artwork,
feedURL: url,
categories: categories?.values.map { $0 } ?? []
)
}
}struct EpisodeDTO: Codable {
let id: Int
let title: String
let description: String?
let enclosureUrl: String
let duration: Int
let datePublished: Int
let image: String?
func toDomain() -> Episode {
Episode(
id: id,
title: title,
description: description,
audioURL: enclosureUrl,
duration: duration,
publishDate: Date(timeIntervalSince1970: TimeInterval(datePublished)),
artworkURL: image
)
}
}ViewModel
↓
UseCase
↓
Repository
↓
NetworkService
↓
URLSession
↓
PodcastIndex API
↓
JSON Response
↓
DTO (Decodable)
↓
Entity (Domain Model)
↓
ViewModel
↓
View
enum NetworkError: Error, LocalizedError {
case invalidURL
case invalidResponse
case decodingError
case serverError(String)
var errorDescription: String? {
switch self {
case .invalidURL:
return "Invalid URL"
case .invalidResponse:
return "Invalid response from server"
case .decodingError:
return "Failed to decode response"
case .serverError(let message):
return message
}
}
}{
"status": "true",
"feeds": [...],
"count": 20,
"query": "tech",
"description": "Found matching feeds."
}{
"status": "false",
"error": "Invalid API key",
"description": "The API key provided is not valid."
}$searchText
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.removeDuplicates()
.sink { searchTerm in
// Perform search
}Benefit: Reduces API calls while user is typing
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 30
configuration.timeoutIntervalForResource = 60Benefit: Prevents hanging requests
func request<T: Decodable>(endpoint: APIConstants.Endpoint) async throws -> T {
let (data, response) = try await session.data(for: request)
return try decoder.decode(T.self, from: data)
}Benefit: Clean, readable asynchronous code
- HTTPS Only: All requests use HTTPS
- Authentication: SHA1 hash-based auth
- No Hardcoded Secrets: API keys in constants (should be env vars)
- Timeout Protection: Request timeouts configured
- Error Handling: Proper error messages
- Environment Variables: Move API keys to environment
- Certificate Pinning: Pin API certificates
- Rate Limiting: Implement client-side rate limiting
- Caching: Cache responses to reduce API calls
- Analytics: Track API errors and performance
- Free Tier: Reasonable use policy
- No Hard Limits: But be respectful
- Recommended: Cache responses when possible
- Cache Results: Don't fetch same data repeatedly
- Batch Requests: Combine when possible
- Handle Errors: Gracefully handle failures
- User Agent: Always include User-Agent header
- Attribution: Credit PodcastIndex in your app
# Test authentication
curl -X GET \
'https://api.podcastindex.org/api/1.0/podcasts/trending?max=5' \
-H 'X-Auth-Key: YOUR_KEY' \
-H 'X-Auth-Date: 1716163200' \
-H 'Authorization: YOUR_HASH' \
-H 'User-Agent: iPodcast/1.0'// Mock NetworkService
class MockNetworkService: NetworkService {
var mockResponse: Any?
override func request<T: Decodable>(endpoint: APIConstants.Endpoint) async throws -> T {
return mockResponse as! T
}
}
// Test Repository
func testGetTrendingPodcasts() async throws {
let mockService = MockNetworkService()
mockService.mockResponse = PodcastResponse(feeds: [...])
let repository = PodcastRepository(networkService: mockService)
let podcasts = try await repository.getTrendingPodcasts()
XCTAssertEqual(podcasts.count, 5)
}Cause: Wrong API key or not set
Solution:
- Check
APIConstants.swift - Verify credentials from PodcastIndex
- Ensure no extra spaces
Cause: Incorrect SHA1 hash
Solution:
- Verify hash generation logic
- Check timestamp format
- Ensure correct concatenation order
Cause: Search term too specific or no matches
Solution:
- Try broader search terms
- Check API status
- Verify internet connection
Cause: API response structure changed
Solution:
- Check API documentation
- Update DTO models
- Add optional fields
Current Version: 1.0
Base URL: https://api.podcastindex.org/api/1.0
Last Updated: May 2026
For API issues:
- Check API Documentation
- Visit PodcastIndex Forum
- Email: info@podcastindex.org
Happy API Integration! 🚀