SILICON ZOMBIES · API V1

Web, Flutter, and billing integration

The server is authoritative for identity, role, Human/Zombie VIP gating, and billing. Send a bearer session token only from secure storage; do not infer VIP access from a completed client payment.

Response and error handlers

Successful JSON responses use { data: ... }. Errors use { error: { code, message, details?, requestId? } }; retain requestId for support, but show only the safe message. JSON mutations require Content-Type: application/json. Invalid fields return 422, missing auth 401, forbidden actions 403, conflicts 409, and temporary integrations 503.

class ApiException implements Exception {
  ApiException(this.status, this.code, this.message, this.requestId);
  final int status;
  final String code;
  final String message;
  final String? requestId;
}

class ZombiesApi {
  ZombiesApi(this.baseUrl, this.token);
  final String baseUrl;
  final String token; // Read from flutter_secure_storage.

  Future<dynamic> request(String method, String path, {Object? body}) async {
    try {
      final response = await http.Request(method, Uri.parse('$baseUrl/api/v1$path'))
        ..headers.addAll({
          'Accept': 'application/json',
          'Authorization': 'Bearer $token',
          if (body != null) 'Content-Type': 'application/json',
        })
        ..body = body == null ? '' : jsonEncode(body);
      final streamed = await response.send().timeout(const Duration(seconds: 20));
      final raw = await streamed.stream.bytesToString();
      final json = raw.isEmpty ? <String, dynamic>{} : jsonDecode(raw) as Map<String, dynamic>;
      if (streamed.statusCode >= 400) {
        final error = json['error'] as Map<String, dynamic>? ?? const {};
        throw ApiException(streamed.statusCode, error['code'] as String? ?? 'HTTP_ERROR',
          error['message'] as String? ?? 'Request failed.', error['requestId'] as String?);
      }
      return json['data'];
    } on TimeoutException {
      throw ApiException(0, 'NETWORK_TIMEOUT', 'Please try again.', null);
    } on SocketException {
      throw ApiException(0, 'NETWORK_OFFLINE', 'Check your connection.', null);
    }
  }
}

Hosted web checkout

After approval, call POST /billing/checkout with { "plan": "monthly" | "annual" }, then navigate to the returned checkoutUrl. Stripe-hosted Checkout shows eligible wallets, including Apple Pay and Google Pay, from the Stripe Dashboard configuration. On the success URL, refresh GET /billing/status; it can remain pending until the signed webhook records a paid subscription.

Flutter PaymentSheet: Android and eligible iOS

Initialize flutter_stripe once with the publishable key and, on iOS, the configured merchant identifier. The endpoint returns only short-lived PaymentSheet material after authentication and VIP approval. The iOS route is disabled by default because digital subscriptions commonly require Apple in-app purchase; enable it only after confirming current App Store eligibility for the storefront.

Future<void> startVipPayment(ZombiesApi api, String plan, TargetPlatform platform) async {
  final platformName = platform == TargetPlatform.iOS ? 'ios' : 'android';
  final data = await api.request('POST', '/billing/mobile-payment-sheet', body: {
    'plan': plan, 'platform': platformName,
  }) as Map<String, dynamic>;

  await Stripe.instance.initPaymentSheet(
    paymentSheetParameters: SetupPaymentSheetParameters(
      merchantDisplayName: 'Silicon Zombies',
      paymentIntentClientSecret: data['paymentIntentClientSecret'] as String,
      customerId: data['customerId'] as String,
      customerEphemeralKeySecret: data['ephemeralKeySecret'] as String,
      returnURL: 'siliconzombies://stripe-redirect',
      style: ThemeMode.dark,
      applePay: const PaymentSheetApplePay(merchantCountryCode: 'US'),
      googlePay: PaymentSheetGooglePay(
        merchantCountryCode: 'US',
        currencyCode: 'USD',
        testEnv: kDebugMode,
      ),
    ),
  );
  await Stripe.instance.presentPaymentSheet();

  // Never grant access here. The verified Stripe webhook changes the tier.
  await api.request('GET', '/billing/status');
}

For Apple Pay, register the merchant ID and verified domain in Stripe and enable the Apple Pay capability in Xcode. For Google Pay, enable it in Stripe and use a physical/device-supported wallet during testing. PaymentSheet can omit a wallet when the device, country, currency, or Dashboard settings are ineligible.

Resource handlers

Endpoint contract

Machine-readable OpenAPI is available at /openapi.json.