TemplatesDocs
All templates

Neobank

Business banking on the Whop API: balances, corporate cards and money movement.
Live iPhone simulator

Code

$git clone https://github.com/whopio/templates.git && cd templates/ios/neobank
API/NeobankEndpoints.swift
protocol NeobankEndpoints: Sendable {
    func account() async throws -> Account
    func financialActivity(accountID: String) async throws -> Paginated<LedgerLine>
    func cards(accountID: String) async throws -> [Card]
    func cardTransactions(accountID: String) async throws -> [CardTransaction]
    func createTransfer(originID: String, destinationID: String, amount: Double, notes: String?) async throws -> Transfer
    func updateCard(_ cardID: String, frozen: Bool) async throws -> Card
}
API/HTTPEndpoints.swift
struct HTTPEndpoints: NeobankEndpoints {
    static let baseURL = URL(string: "https://api.whop.com/api/v1")!
    static let apiVersionDate = "2026-08-03"

    let accessToken: String

    private var decoder: JSONDecoder {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return decoder
    }

    func account() async throws -> Account {
        try await get("/accounts/me")
    }

    func cards(accountID: String) async throws -> [Card] {
        let page: Paginated<Card> = try await get("/cards", query: ["account_id": accountID])
        return page.data
    }

    private func get<T: Decodable>(_ path: String, query: [String: String] = [:]) async throws -> T {
        var components = URLComponents(url: Self.baseURL.appending(path: path), resolvingAgainstBaseURL: false)!
        if !query.isEmpty {
            components.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) }
        }
        return try await perform(URLRequest(url: components.url!), path: path)
    }

    private func perform<T: Decodable>(_ request: URLRequest, path: String) async throws -> T {
        var request = request
        request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
        request.setValue(Self.apiVersionDate, forHTTPHeaderField: "Api-Version-Date")

        let (data, response) = try await URLSession.shared.data(for: request)
        let status = (response as? HTTPURLResponse)?.statusCode ?? 0

        if status == 401 || status == 403 { throw NeobankError.unauthorized }
        guard (200 ..< 300).contains(status) else { throw NeobankError.http(status: status, path: path) }

        return try decoder.decode(T.self, from: data)
    }
}
NeobankApp.swift
@main
struct NeobankApp: App {
    var body: some Scene {
        WindowGroup {
            RootView(endpoints: NeobankConfiguration.endpoints())
        }
    }
}

// Swap the protocol for fixtures, a cache, or your own backend proxy — every screen follows.
enum NeobankConfiguration {
    static func endpoints() -> any NeobankEndpoints {
        guard let token = value("NEOBANK_ACCESS_TOKEN"), !usesFixtures else {
            return FixtureEndpoints()
        }
        return HTTPEndpoints(accessToken: token)
    }
}