Standard iOS Integration Guide

    Accept payments in iPhone and iPad apps, with built-in support for Apple Pay. If you need help after reading this, search our documentation or check out answers to common questions. You can even chat live with other developers in #stripe on freenode.

    This guide will take you through building your app's payment flow using STPPaymentContext, a class designed to make building your app's checkout flow as easy as possible. It handles collecting, saving, and reusing your user's payment details, and can also be used to collect shipping info. If you find that this isn't flexible enough for your needs, all of the subcomponents that STPPaymentContext uses under the hood are available for you to use as well. For more on these subcomponents, please read our custom iOS integration guide.

    Prepare your API

    Our prebuilt UI elements operate on the assumption that you have a single Customer object for each of your users. We strongly recommend you create this Customer at the same time you create your user on your own backend, so that every user is guaranteed to have an associated Customer (this is fine even if you don't collect your user's payment information when they sign up — it's totally OK to have a Customer without any attached cards).

    In order for our prebuilt UI elements to function, you'll need to provide them with information about your user's saved payment information. To fetch this information, you should expose three APIs on your backend for your iOS app to communicate with.

    Endpoint 1: Retrieve the Customer object for the currently logged-in user

    This method is called to populate the user's list of payment methods in our UI. For this, you can just call the Stripe Retrieve Customer API and return its unmodified response as JSON.

    require "json"
    
    # Using Sinatra (http://www.sinatrarb.com/)
    get "/customer" do
      begin
        customer_id = "..." # Load the Stripe Customer ID for your logged in user
        customer = Stripe::Customer.retrieve(customer_id)
      rescue Stripe::StripeError => e
        status 402
        return "Error retrieving customer: #{e.message}"
      end
      status 200
      content_type :json
      customer.to_json
    end
    import json
    from django.http import HttpResponse
    from django.http import JsonResponse
    
    # Using Django (https://www.djangoproject.com/)
    def my_customer_view(request):
      try:
        customer_id = "..." # Load the Stripe Customer ID for your logged in user
        customer = stripe.Customer.retrieve(customer_id)
        return JsonResponse(customer)
      except stripe.error.StripeError as e:
        return HttpResponse(status=402)
    try {
      $customer_id = "..."; // Load the Stripe Customer ID for your logged in user
      $customer = \Stripe\Customer::retrieve($customer_id);
      header('Content-Type: application/json');
      echo $customer->jsonSerialize();
    } catch(\Stripe\Error $e) {
      http_response_code(402);
    }
    // Using Express (http://expressjs.com/)
    app.get('/customer', function(request, response) {
      var customerId = '...'; // Load the Stripe Customer ID for your logged in user
      stripe.customers.retrieve(customerId, function(err, customer) {
        if (err) {
          response.status(402).send('Error retrieving customer.');
        } else {
          response.json(customer);
        }
      })
    });
    // Using Spark framework (http://sparkjava.com)
    import com.google.gson.Gson;
    
    private Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
    
    get("/customer", (request, response) -> {
      String customerID = "..."; // Load the Stripe Customer ID for your logged in user
        try {
          Customer customer = Customer.retrieve(customerID);
          response.status(200);
          return gson.toJson(customer);
        } catch (StripeException e) {
          response.status(402);
          return "";
        }
    });

    Endpoint 2: Attach a new payment source to the Customer for the currently-logged in user

    This method is called when the user adds a new payment method via our UI. For this, you should call the Stripe Create Card API and return its unmodified response as JSON.

    require "json"
    
    # Using Sinatra (http://www.sinatrarb.com/)
    post "/customer/sources" do
      begin
        customer_id = "..." # Load the Stripe Customer ID for your logged in user
        customer = Stripe::Customer.retrieve(customer_id)
        customer.sources.create({:source => params[:source]})
        status 200
      rescue Stripe::StripeError => e
        status 402
        return "Error retrieving customer: #{e.message}"
      end
    end
    import json
    from django.http import HttpResponse
    
    # Using Django (https://www.djangoproject.com/)
    def my_customer_source_view(request):
      try:
        customer_id = "..." # Load the Stripe Customer ID for your logged in user
        customer = stripe.Customer.retrieve(customer_id)
        source = request.POST['source']
        customer.sources.create(source=source)
        return HttpResponse(status=200)
      except stripe.error.StripeError as e:
        return HttpResponse(status=402)
    $customer_id = "..."; // Load the Stripe Customer ID for your logged in user
    
    try {
      $customer = \Stripe\Customer::retrieve($customer_id);
      $customer->sources->create(array("source" => $_POST["source"]));
      http_response_code(200);
    } catch(\Stripe\Error $e) {
      http_response_code(402);
    }
    // Using Express (http://expressjs.com/)
    app.post('/customer/sources', function(request, response) {
      var customerId = '...'; // Load the Stripe Customer ID for your logged in user
      stripe.customers.createSource(customerId, {
        source: request.body.source
      }, function(err, source) {
        if (err) {
          response.status(402).send('Error attaching source.');
        } else {
          response.status(200).end();
        }
      });
    });
    // Using Spark framework (http://sparkjava.com)
    post("/customer/sources", (request, response) -> {
      String customerID = "..."; // Load the Stripe Customer ID for your logged in user
        try {
          Customer customer = Customer.retrieve(customerID);
          Map<String, Object> params = new HashMap<String, Object>();
          params.put("source", request.params("source"));
          customer.getSources().create(params);
          response.status(200);
        } catch (StripeException e) {
          response.status(402);
        }
        return "";
    });

    Endpoint 3: Select a new default payment source on the Customer for the currently logged-in user

    This method is called when the user changes their selected payment method in our UI. For this, you should call the Stripe Customer Update API and specify a new default_source.

    require "json"
    
    # Using Sinatra (http://www.sinatrarb.com/)
    post "/customer/default_source" do
      begin
        customer_id = "..." # Load the Stripe Customer ID for your logged in user
        customer = Stripe::Customer.retrieve(customer_id)
        customer.default_source = params[:default_source]
        customer.save
        status 200
      rescue Stripe::StripeError => e
        status 402
        return "Error retrieving customer: #{e.message}"
      end
    end
    import json
    from django.http import HttpResponse
    
    # Using Django (https://www.djangoproject.com/)
    def my_customer_default_source_view(request):
      try:
        customer_id = "..." # Load the Stripe Customer ID for your logged in user
        customer = stripe.Customer.retrieve(customer_id)
        customer.default_source = request.POST['default_source']
        customer.save()
        return HttpResponse(status=200)
      except stripe.error.StripeError as e:
        return HttpResponse(status=402)
    $customer_id = "..." // Load the Stripe Customer ID for your logged in user
    
    try {
      $customer = \Stripe\Customer::retrieve($customer_id);
      $customer->default_source = $_GET["default_source"];
      $customer->save();
      http_response_code(200);
    } catch(\Stripe\Error $e) {
      http_response_code(402);
    }
    // Using Express (http://expressjs.com/)
    app.post('/customer/default_source', function(request, response) {
      var customerId = '...'; // Load the Stripe Customer ID for your logged in user
      stripe.customers.update(customerId, {
        default_source: request.body.defaultSource
      }, function(err, customer) {
        if (err) {
          response.status(402).send('Error setting default source.');
        } else {
          response.status(200).end();
        }
      });
    });
    // Using Spark framework (http://sparkjava.com)
    post("/customer/sources", (request, response) -> {
      String customerID = "..." # Load the Stripe Customer ID for your logged in user
        try {
          Customer customer = Customer.retrieve(customerID);
          Map<String, Object> updateParams = new HashMap<String, Object>();
          updateParams.put("default_source", request.params("default_source"));
          customer.update(updateParams);
          response.status(200);
        } catch (StripeException e) {
          response.status(402);
        }
        return "";
    });

    Next, you'll need a way for your iOS app to communicate with these endpoints. To make this easier, we've introduced the concept of an "API adapter" – a class that communicates with your backend API and returns objects that the prebuilt Stripe UI knows how to process. To create your API adapter, you should make a class that conforms to the STPBackendAPIAdapter protocol, which has three required methods (note how they align with the new endpoints you've exposed on your backend API):

    • - retrieveCustomer:completion: Read more
    • - attachSourceToCustomer:completion: Read more
    • - selectDefaultCustomerSource:completion: Read more

    You can consult the Example App in our SDK to see this in practice.

    Implement your app's checkout flow using STPPaymentContext

    Our SDK provides a class called STPPaymentContext, which is designed to make building your app's checkout flow as easy as possible. It handles collecting, saving, and reusing your user's payment details, and can also be used to collect shipping info. Think of it as the data source for your checkout view controller – it handles asynchronously retrieving the data you need, and notifies its delegate when its UI should change. Most importantly, it lets you build a single integration to accept payments via both credit card and Apple Pay instead of having multiple code paths.

    Setting the delegate and host view controller

    To work with STPPaymentContext, you'll need to write a class that conforms to STPPaymentContextDelegate. (Note, the code samples in this section are simply examples – your own implementation may differ depending on the structure of your app). STPPaymentContextDelegate has 4 required methods:

    - paymentContextDidChange:

    This method is called, as you might expect, when the payment context's contents change, e.g. when the user selects a new payment method or enters shipping info. This is a good place to update your UI:

    func paymentContextDidChange(_ paymentContext: STPPaymentContext) {
        self.activityIndicator.animating = paymentContext.loading
        self.paymentButton.enabled = paymentContext.selectedPaymentMethod != nil
        self.paymentLabel.text = paymentContext.selectedPaymentMethod?.label
        self.paymentIcon.image = paymentContext.selectedPaymentMethod?.image
    }
    - (void)paymentContextDidChange:(STPPaymentContext *)paymentContext {
        self.activityIndicator.animating = paymentContext.loading;
        self.paymentButton.enabled = paymentContext.selectedPaymentMethod != nil;
        self.paymentLabel.text = paymentContext.selectedPaymentMethod.label;
        self.paymentIcon.image = paymentContext.selectedPaymentMethod.image;
    }

    - paymentContext:didCreatePaymentResult:completion:

    This method is called when the user has successfully selected a payment method and completed their purchase. You should pass the contents of the paymentResult object to your backend, which should then finish charging your user using the create charge API. When this API request is finished, call the provided completion block with nil as its only argument if the call succeeded, or, if an error occurred, with that error as the argument instead.

    func paymentContext(_ paymentContext: STPPaymentContext,
      didCreatePaymentResult paymentResult: STPPaymentResult,
      completion: @escaping STPErrorBlock) {
    
      myAPIClient.createCharge(paymentResult.source.stripeID, completion: { (error: Error?) in
          if let error = error {
              completion(error)
          } else {
              completion(nil)
          }
      })
    }
    - (void)paymentContext:(STPPaymentContext *)paymentContext
    didCreatePaymentResult:(STPPaymentResult *)paymentResult
                completion:(STPErrorBlock)completion {
        [self.apiClient createCharge:paymentResult.source.stripeID completion:^(NSError *error) {
            if (error) {
                completion(error);
            } else {
                completion(nil);
            }
        }];
    }

    - paymentContext:didFinishWithStatus:error:

    This method is called after the previous method, when any auxiliary UI that has been displayed (such as the Apple Pay dialog) has been dismissed. You should inspect the returned status and show an appropriate message to your user. For example:

    func paymentContext(_ paymentContext: STPPaymentContext,
      didFinishWithStatus status: STPPaymentStatus,
      error: Error?) {
    
        switch status {
        case .error:
            self.showError(error)
        case .success:
            self.showReceipt()
        case .userCancellation:
            return // Do nothing
        }
    }
    - (void)paymentContext:(STPPaymentContext *)paymentContext
       didFinishWithStatus:(STPPaymentStatus)status
                     error:(NSError *)error {
        switch (status) {
            case STPPaymentStatusSuccess:
                [self showReceipt];
            case STPPaymentStatusError:
                [self showError:error];
            case STPPaymentStatusUserCancellation:
                return; // Do nothing
        }
    }

    - paymentContext:didFailToLoadWithError:

    This method is called in the rare case that the payment context's initial loading call fails, usually due to lack of internet connectivity. You should dismiss your checkout page when this occurs and invite the user to try again. You can also optionally attempt to try again by calling retryLoading on the payment context.

    func paymentContext(_ paymentContext: STPPaymentContext,
      didFailToLoadWithError error: Error) {
        self.navigationController?.popViewController(animated: true)
        // Show the error to your user, etc.
    }
    - (void)paymentContext:(STPPaymentContext *)paymentContext didFailToLoadWithError:(NSError *)error {
         [self.navigationController popViewControllerAnimated:YES];
         // Show the error to your user, etc.
    }

    - paymentContext:didUpdateShippingAddress:completion

    If you're using STPPaymentContext to collect shipping info, this method will be called after your user enters a shipping address. Here, you should validate the returned address and determine the shipping methods available for that address. If the address is valid, you should call the provided completion block with a status of STPShippingStatusValid, nil for the error argument, an array of shipping methods, and a selected shipping method. If you don't need to collect a shipping method, you should just pass nil for the shipping methods and selected shipping method. If the address is invalid, you should call the completion block with a status of STPShippingStatusInvalid, an error object describing the issue with the address, and nil for the shipping methods and selected shipping method. Note that providing an error object is optional – if you omit it, the user will simply see an alert with the message "Invalid Shipping Address".

    func paymentContext(_ paymentContext: STPPaymentContext, didUpdateShippingAddress address: STPAddress, completion: @escaping STPShippingMethodsCompletionBlock) {
        let upsGround = PKShippingMethod()
        upsGround.amount = 0
        upsGround.label = "UPS Ground"
        upsGround.detail = "Arrives in 3-5 days"
        upsGround.identifier = "ups_ground"
        let fedEx = PKShippingMethod()
        fedEx.amount = 5.99
        fedEx.label = "FedEx"
        fedEx.detail = "Arrives tomorrow"
        fedEx.identifier = "fedex"
    
        if address.country == "US" {
            completion(.valid, nil, [upsGround, fedEx], upsGround)
        }
        else {
            completion(.invalid, nil, nil, nil)
        }
    }
    - (void)paymentContext:(STPPaymentContext *)paymentContext didUpdateShippingAddress:(STPAddress *)address completion:(STPShippingMethodsCompletionBlock)completion {
        PKShippingMethod *upsGround = [PKShippingMethod new];
        upsGround.amount = [NSDecimalNumber decimalNumberWithString:@"0"];
        upsGround.label = @"UPS Ground";
        upsGround.detail = @"Arrives in 3-5 days";
        upsGround.identifier = @"ups_ground";
        PKShippingMethod *fedEx = [PKShippingMethod new];
        fedEx.amount = [NSDecimalNumber decimalNumberWithString:@"5.99"];
        fedEx.label = @"FedEx";
        fedEx.detail = @"Arrives tomorrow";
        fedEx.identifier = @"fedex";
        if ([address.country isEqualToString:@"US"]) {
            completion(STPShippingStatusValid, nil, @[upsGround, fedEx], upsGround);
        }
        else {
            completion(STPShippingStatusInvalid, nil, nil, nil);
        }
    }

    Once you've written a class that conforms to STPPaymentContextDelegate, you can initialize STPPaymentContext with your API adapter. After you do this, set its delegate and hostViewController properties (these will usually be the same object, a UIViewController instance). You should also set the payment context's paymentAmount property, which will be displayed to your user in the Apple Pay dialog (you can change this later, if the amount of the user's purchase changes).

    init() {
        // Here, MyAPIAdapter is your class that implements STPBackendAPIAdapter (see above)
        self.paymentContext = STPPaymentContext(apiAdapter: MyAPIAdapter())
        super.init(nibName: nil, bundle: nil)
        self.paymentContext.delegate = self
        self.paymentContext.hostViewController = self
        self.paymentContext.paymentAmount = 5000 // This is in cents, i.e. $50 USD
    }
    - (instancetype)init {
        self = [super initWithNibName:nil bundle:nil];
        if (self) {
           // Here, MyAPIAdapter is your class that implements STPBackendAPIAdapter (see above)
           id<STPBackendAPIAdapter> apiAdapter = [[MyAPIAdapter alloc] init];
           self.paymentContext = [[STPPaymentContext alloc] initWithAPIAdapter:apiAdapter];
           self.paymentContext.delegate = self;
           self.paymentContext.hostViewController = self;
           self.paymentContext.paymentAmount = 5000 // This in cents, i.e. $50 USD
        }
        return self;
    }

    Let your user select their payment method

    When you'd like to give your user the chance to enter or change their payment method (say, they tapped a button on your checkout page that lets them do this), STPPaymentContext can do this for you automatically. You can specify whether you'd like the payment selector view controller to be presented modally, or pushed onto a UINavigationController stack:

    // If you prefer a modal presentation
    func choosePaymentButtonTapped() {
        self.paymentContext.presentPaymentMethodsViewController()
    }
    
    // If you prefer a navigation transition
    func choosePaymentButtonTapped() {
        self.paymentContext.pushPaymentMethodsViewController()
    }
    // If you prefer a modal presentation
    - (void)choosePaymentButtonTapped {
        [self.paymentContext presentPaymentMethodsViewController];
    }
    
    // If you prefer a navigation transition
    - (void)choosePaymentButtonTapped {
        [self.paymentContext pushPaymentMethodsViewController];
    }

    This will set up and present an STPPaymentMethodsViewController on the payment context's hostViewController. If the user doesn't have any stored payment methods, it'll automatically prompt them to enter one. If the user has ever made a purchase from another app that uses STPPaymentContext or from Stripe Checkout on the web (and opted in, of course), they'll have the option to autofill their payment details. After entering their email address, they'll be sent an SMS with a code and prompted to enter it in a form that appears. We've found this to have a significant positive impact on conversion rates, but if you'd like to disable it, just add the following code to your app delegate:

    STPPaymentConfiguration.shared().smsAutofillDisabled = true
    [[STPPaymentConfiguration sharedConfiguration] setSmsAutofillDisabled:YES];

    Let your user enter their shipping info

    When you'd like to give your user the chance to enter or change their shipping address and shipping method, STPPaymentContext can do this for you automatically. You can specify whether you'd like the shipping view controller to be presented modally, or pushed onto a UINavigationController stack:

    // If you prefer a modal presentation
    func shippingButtonTapped() {
        self.paymentContext.presentShippingViewController()
    }
    
    // If you prefer a navigation transition
    func shippingButtonTapped() {
        self.paymentContext.presentShippingViewController()
    }
    // If you prefer a modal presentation
    - (void)choosePaymentButtonTapped {
        [self.paymentContext presentPaymentMethodsViewController];
    }
    
    // If you prefer a navigation transition
    - (void)choosePaymentButtonTapped {
        [self.paymentContext pushPaymentMethodsViewController];
    }

    This will set up and present an STPShippingAddressViewController on the payment context's hostViewController. Once the user enters a valid shipping address, they'll be taken to an STPShippingMethodsViewController. After they select a shipping method, both view controllers will be dismissed or popped off the hostViewController's stack.

    Finishing a payment

    Finally, when your user taps "Buy", just call requestPayment on your payment context. It'll display any required UI (such as the Apple Pay dialog) and call the appropriate methods on its delegate as your user finishes their payment.

    func payButtonTapped() {
        self.paymentContext.requestPayment()
    }
    - (void)payButtonTapped {
        [self.paymentContext requestPayment];
    }

    Customizing the look-and-feel of Stripe UI elements

    We built the payment selector so that you can define your own appearance and apply the attributes you want to customize from the default theme that we provide.

    Stripe iOS UI Theming

    You can override any of the properties from the default theme, typically in your AppDelegate:

    STPTheme.default().accentColor = UIColor.blue
    [[STPTheme defaultTheme] setAccentColor:[UIColor blueColor]];

    Here is a list of properties you can customize and their respective usage inside the payment selector UI:

    Property Name Description
    primaryBackgroundColor Background color for any views in this theme
    secondaryBackgroundColor Background color for any supplemental views inside a view (for example the cells of a table view)
    primaryForegroundColor Text color for any important labels in a view
    secondaryForegroundColor Text color for any supplementary labels in a view
    accentColor Color for any buttons and other elements on a view that are important to highlight
    errorColor Color for rendering any error messages or views
    font Font to be used for all views
    emphasisFont Medium-weight font to be used for all bold text in views

    We also let you customize all the fonts used in this payment selector flow. By default, the SDK will use the system font at different weight and sizes. To override the defaults, set the font and emphasisFont properties and the SDK will automatically use your choice instead and infer the rest.

    More information

    If you'd like more help, we provide an example app that implements all of the code in this tutorial.

    In addition, if you'd like to implement a custom payment flow, all of the sub-components of STPPaymentContext are usable individually. You can read more about them in our custom iOS integration guide.