<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vitaly Davydov</title>
    <description>The latest articles on DEV Community by Vitaly Davydov (@iwitaly).</description>
    <link>https://dev.to/iwitaly</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F394791%2Ffa373e88-eb66-494a-bcfe-31e5d1ddc3c1.jpg</url>
      <title>DEV Community: Vitaly Davydov</title>
      <link>https://dev.to/iwitaly</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iwitaly"/>
    <language>en</language>
    <item>
      <title>What’s new about StoreKit 2 server API</title>
      <dc:creator>Vitaly Davydov</dc:creator>
      <pubDate>Mon, 12 Jul 2021 13:07:18 +0000</pubDate>
      <link>https://dev.to/iwitaly/what-s-new-about-storekit-2-api-and-how-apple-simplified-integration-of-in-app-purchases-18p5</link>
      <guid>https://dev.to/iwitaly/what-s-new-about-storekit-2-api-and-how-apple-simplified-integration-of-in-app-purchases-18p5</guid>
      <description>&lt;p&gt;Apple introduced a new version of StoreKit 2 during WWDC 2021 that took place recently. This is a framework responsible for making purchases in iOS. A share of apps with in-app purchase and subscription features grows steadily, and Apple significantly simplified integration of in-app purchases into the app by releasing StoreKit 2. Today, we will consider working with StoreKit 2 on the part of the server, in other words, with the help of App Store Server API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request authentication
&lt;/h2&gt;

&lt;p&gt;In the current API version, you need Shared Secret to send a request. This is a secret fixed string that you can get in App Store Connect. A new version of API uses JSON Web Token (JWT) standard for request authentication.   &lt;/p&gt;

&lt;h3&gt;
  
  
  Key generation
&lt;/h3&gt;

&lt;p&gt;First of all, &lt;a href="https://developer.apple.com/documentation/appstoreserverapi/creating_api_keys_to_use_with_the_app_store_server_api" rel="noopener noreferrer"&gt;create a private key&lt;/a&gt; that will be used to authorize the requests. Open App Store Connect and go to the Users and Access section, then to Keys tab. Select In-App Purchase key type. Download a new key. You will also need its ID – you can copy it on the same page as Issue ID which can be found in the App Store Connect API tab.&lt;br&gt;
Creation of a private key for working with App Store Server API&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyntpzsq63rdccquuh29f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyntpzsq63rdccquuh29f.png" alt="StoreKit 2 API Creation of a private key"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Creating a token
&lt;/h3&gt;

&lt;p&gt;The next step is to create a token that will be used to authorize the requests. This process is described in detail in &lt;a href="https://developer.apple.com/documentation/appstoreserverapi/generating_tokens_for_api_requests" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;, so there’s no reason to pay too much attention to it. Here’s an example of a ready-made implementation for Python. It’s worth noting that it makes no sense to generate a new token for every new request. When creating a token, you set its lifetime at up to 60 minutes and use the same token during this period.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;authlib.jose&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;

&lt;span class="n"&gt;BUNDLE_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;com.adapty.sample_app&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;ISSUER_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;4336a124-f214-4d40-883b-6db275b5e4aa&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;KEY_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;J65UYBDA74&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;PRIVATE_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="s"&gt;
-----BEGIN PRIVATE KEY-----
MIGTAgMGByqGSMBHkAQQgR/fR+3Lkg4...
-----END PRIVATE KEY-----
&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;

&lt;span class="n"&gt;issue_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;expiration_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;issue_time&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="c1"&gt;# 1 hour expiration
&lt;/span&gt;

&lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;alg&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ES256&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;kid&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;KEY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;typ&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;JWT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;iss&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ISSUER_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;iat&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;issue_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;exp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;expiration_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;aud&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;appstoreconnect-v1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nonce&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
 &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bid&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BUNDLE_ID&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;token_encoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PRIVATE_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;token_decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token_encoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;authorization_header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token_decoded&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Signed transactions
&lt;/h2&gt;

&lt;p&gt;In a new version of API, all transactions returned in JSON Web Signature (JWS) standard. This is a string consisting of three parts divided by dots.  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Base64 header.&lt;/li&gt;
&lt;li&gt;Base64 transaction payload.&lt;/li&gt;
&lt;li&gt;Transaction signature.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;Base64(header) + "." + Base64(payload) + "." + sign(Base64(header) + "." + Base64(payload))&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Transaction header
&lt;/h3&gt;

&lt;p&gt;A header is needed to make sure the transaction is authentic. &lt;code&gt;Alg&lt;/code&gt; key contains an encryption algorithm, &lt;code&gt;x5c&lt;/code&gt; key contains a certificate chain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"kid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AMP/DEV"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"x5c"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"MIIEO..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"MIIDK..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Transaction payload
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transactionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1000000831360853"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"originalTransactionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1000000806937552"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"webOrderLineItemId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1000000063561721"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bundleId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.adapty.sample_app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"productId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"basic_subscription_1_month"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subscriptionGroupIdentifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"27636320"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"purchaseDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1624446341000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"originalPurchaseDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1619686337000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expiresDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1624446641000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Auto-Renewable Subscription"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"appAccountToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fd12746f-2d3a-46c8-bff8-55b75ed06aca"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inAppOwnershipType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PURCHASED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signedDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1624446484882&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"offerType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"offerIdentifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"basic_subscription_1_month.pay_as_you_go.3_months"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apple changed and extended the transaction format. From my point of view, now, it’s more convenient to work with them. You can learn details about a new format in &lt;a href="https://developer.apple.com/documentation/appstoreserverapi/jwstransactiondecodedpayload" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;. Below, I will describe the most important changes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Apple added &lt;code&gt;appAccountToken&lt;/code&gt; field, which contains your system’s user ID. This ID must be in UUID format, it is set in the mobile app when a purchase is being initialized. If it is set, it will be returned in all transactions in this chain (renewal, billing issues, etc.), and you will easily understand which user made a purchase.  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Apple also added &lt;code&gt;offerType&lt;/code&gt; and &lt;code&gt;offerIdentifier&lt;/code&gt; fields that contain the information about a used offer (if any). Here are values for offerType field: &lt;br&gt;
1 — intro offer (available only for the users without active or expired subscriptions);&lt;br&gt;
2 — promo offer (available only for current and expired subscriptions); &lt;br&gt;
3 — offer code. If a promo offer or offer code was used, &lt;code&gt;offerIdentifier&lt;/code&gt; key will contain the ID of the used offer. In the past, it was impossible to track the use of the offer on the server-side, this worsened the analytics. Now, you can use offer codes for analytics.   &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Apple added &lt;code&gt;inAppOwnershipType&lt;/code&gt; field, which helps to understand whether a user bought a product or accessed it thanks to a family subscription. Possible values:&lt;br&gt;
&lt;code&gt;PURCHASED&lt;/code&gt;&lt;br&gt;
&lt;code&gt;FAMILY_SHARED&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Another new field – &lt;code&gt;type&lt;/code&gt; – includes transaction type. Possible values:&lt;br&gt;
&lt;code&gt;Auto-Renewable Subscription&lt;/code&gt;&lt;br&gt;
&lt;code&gt;Non-Consumable&lt;/code&gt;&lt;br&gt;
&lt;code&gt;Consumable&lt;/code&gt;&lt;br&gt;
&lt;code&gt;Non-Renewing Subscription&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Cancellation_date&lt;/code&gt; and &lt;code&gt;cancellation_reason&lt;/code&gt; fields have new names now: &lt;code&gt;revocationDate&lt;/code&gt; and &lt;code&gt;revocationReason&lt;/code&gt;. As a reminder, they contain a date and a reason for subscription revocation as a result of a refund, so the new name looks more logical.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;All keys return in camelCase format (just like in all App Store Server API requests).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;All dates are displayed in Unix timestamp format in milliseconds.   &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  User subscription status
&lt;/h2&gt;

&lt;p&gt;To check the current user’s &lt;a href="https://developer.apple.com/documentation/appstoreserverapi/get_all_subscription_statuses" rel="noopener noreferrer"&gt;subscription status&lt;/a&gt;, send a GET request to &lt;code&gt;https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{originalTransactionId}&lt;/code&gt;, where &lt;code&gt;{originalTransactionId}&lt;/code&gt; is the ID of any transaction chain of the user. In return, you will get transactions with statuses for every group of subscriptions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sandbox"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bundleId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.adapty.sample_app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"subscriptionGroupIdentifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"39636320"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"lastTransactions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"originalTransactionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1000000819078552"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"signedTransactionInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyJraWQiOi..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"signedRenewalInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyJraWQiOi..."&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;status&lt;/code&gt; key displays the current subscription status, based on it, you can decide if you should provide a user with access to the paid features of the app. Possible values:&lt;/p&gt;

&lt;p&gt;1 — subscription is active, a user must be able to access paid features.&lt;br&gt;
2 — subscription has expired, a user must not be able to access paid functions.&lt;br&gt;&lt;br&gt;
3 — the subscription’s status is Billing Retry, meaning that a user didn’t cancel it, but experiences problems with paying. Apple will try to charge the card for 60 days. A user must not be able to access paid functions.&lt;br&gt;&lt;br&gt;
4 — the subscription’s status is Grace Period, meaning that a user didn’t cancel it, but experiences payment issues. Grace Period is on in App Store Connect, so a user must be able to access paid features.&lt;br&gt;&lt;br&gt;
5 — subscription was canceled as a result of a refund, a user must not be able to access paid functions. &lt;br&gt;
SignedTransactionInfo key contains the information about the last transaction in the chain. You can find the details about its format above.&lt;/p&gt;
&lt;h3&gt;
  
  
  Information about subscription renewal
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;SignedRenewalInfo&lt;/code&gt; key contains the information about subscription &lt;a href="https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfodecodedpayload" rel="noopener noreferrer"&gt;renewal&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expirationIntent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"originalTransactionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1000000819078552"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"autoRenewProductId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"basic_subscription_1_month"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"productId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"basic_subscription_1_month"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"autoRenewStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isInBillingRetryPeriod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signedDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1624520884048&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This information allows us to understand what will happen to the subscription during the next pay period. For example, if you see that a user canceled auto-renewal, you can offer them to switch to another subscription plan or provide them a promo offer. It’s convenient to track this kind of events with the help of server notifications, which I’ll tell you about soon.&lt;/p&gt;

&lt;h2&gt;
  
  
  User’s transaction history
&lt;/h2&gt;

&lt;p&gt;To get the user’s &lt;a href="https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history" rel="noopener noreferrer"&gt;transaction history&lt;/a&gt;, send GET request to &lt;code&gt;https://api.storekit.itunes.apple.com/inApps/v1/history/{originalTransactionId}&lt;/code&gt;, where &lt;code&gt;{originalTransactionId}&lt;/code&gt; is the ID of any chain of transactions of the user. In return, you will get an array of transactions sorted by time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"revision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1625872984000_1000000212854038"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bundleId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.adapty.sample_app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sandbox"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hasMore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signedTransactions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"eyJraWQiOiJ..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"joiRVMyNeyX..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"5MnkvOTlOZl..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A request can contain no more than 20 transactions. If a user has more, the &lt;code&gt;hasMore&lt;/code&gt; flag’s value will be &lt;code&gt;true&lt;/code&gt;. If you need the next transaction page, send the request again with the &lt;code&gt;revision&lt;/code&gt; GET parameter containing. It will contain the value from the same key. &lt;/p&gt;

&lt;h2&gt;
  
  
  Server transaction notifications
&lt;/h2&gt;

&lt;p&gt;Server notifications help to get information about new purchases, renewals, billing issues, etc. This helps to build more accurate analytics, as well as simplifies managing the subscriber’s status.&lt;/p&gt;

&lt;p&gt;The existing server notifications (V1) can solve most of the problems, but sometimes they are inconvenient. Mostly, it’s about the situation when you get several notifications for just one action of a user. For example, now, when a user upgrades a subscription, Apple sends two notifications: &lt;code&gt;DID_CHANGE_RENEWAL_STATUS&lt;/code&gt; and &lt;code&gt;INTERACTIVE_RENEWAL&lt;/code&gt;. To process this case currently, you need to save the status somehow and check if the second notification was sent. In a new version of server notifications (V2), there’s only one notification for one action of a user. This is much more convenient.&lt;/p&gt;

&lt;p&gt;The second version of server notifications features new events - &lt;code&gt;OFFER_REDEEMED&lt;/code&gt;, &lt;code&gt;EXPIRED&lt;/code&gt;, and &lt;code&gt;GRACE_PERIOD_EXPIRED&lt;/code&gt;. They make managing subscriber status much easier. &lt;code&gt;SUBSCRIBED&lt;/code&gt; and &lt;code&gt;PRICE_INCREASE&lt;/code&gt;  events are improved events from the first version.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feftls4jiyqm0x465cxqy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feftls4jiyqm0x465cxqy.png" alt="comparison of notification type in Apple API StoreKit and StoreKit 2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Notification types
&lt;/h2&gt;

&lt;p&gt;Notifications now have types, thus, one notification for any action of a user is enough to understand what happened. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feftls4jiyqm0x465cxqy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feftls4jiyqm0x465cxqy.png" alt="Notification types in StoreKit 2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notification types&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notificationType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SUBSCRIBED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subtype"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INITIAL_BUY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sandbox"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bundleId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.adapty.sample_app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"appAppleId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;739104078&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bundleVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"signedTransactionInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyJraWQiOi..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"signedRenewalInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyJraWQiOi..."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Server notifications contain information about a transaction and a renewal in JWS format which I described earlier.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Working with Sandbox environment
&lt;/h2&gt;

&lt;p&gt;You need to use the URL of the Sandbox environment to test purchases: &lt;code&gt;https://api.storekit-sandbox.itunes.apple.com.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A new version of server notifications isn’t available for testing yet. Once it’s available, it will be possible to specify different URLs for Production and Sandbox notifications. You can choose V2 for Sandbox, and V1 for Production for testing.&lt;/p&gt;

&lt;p&gt;Also, App Store Connect now allows to:&lt;br&gt;
*Clear purchase history for the Sandbox user, meaning that you don’t have to create a new account to do that anymore.   &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Change the store country for the Sandbox user.&lt;/li&gt;
&lt;li&gt;Change Sandbox subscription renewal period, for example, you can make a monthly purchase that lasts 1 hour instead of 5 minutes.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Apple significantly improved working with in-app purchases and subscriptions on the server-side. From my point of view, here’re the most useful new features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full-fledged promo offer and offer code support;
&lt;/li&gt;
&lt;li&gt;Simpler and more informative server notifications;
&lt;/li&gt;
&lt;li&gt;An opportunity to learn about the current subscription’s status with a simple API call;&lt;/li&gt;
&lt;li&gt;Clearing purchase history of the user’s Sandbox.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Switching to a new API won’t be hard, it’s enough to get &lt;code&gt;originalTransactionId&lt;/code&gt; for every receipt. It’s likely it’s already contained in your base.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>discuss</category>
      <category>swift</category>
    </item>
    <item>
      <title>Development, analytics, attribution. Which services to use in 2021?</title>
      <dc:creator>Vitaly Davydov</dc:creator>
      <pubDate>Fri, 09 Apr 2021 15:44:02 +0000</pubDate>
      <link>https://dev.to/iwitaly/development-analytics-attribution-which-services-to-use-in-2021-477b</link>
      <guid>https://dev.to/iwitaly/development-analytics-attribution-which-services-to-use-in-2021-477b</guid>
      <description>&lt;p&gt;Apps don’t exist in a vacuum. While there’s certainly not a great space for things a good team can’t do, some 3rd-party services could be of real help to your apps’ growth.&lt;/p&gt;

&lt;p&gt;In this article, we will dive into valuable services for freemium iOS apps that might help any development or marketing team.&lt;/p&gt;

&lt;p&gt;‍&lt;strong&gt;Let's draw a plan first.&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;An overwhelming majority of mobile apps are monetized through traffic, that is, the purchase of advertisements. I assess that most apps don’t have a website or a web version, and even platform presence is usually limited to iOS. Suppose a company doesn’t have access to a reliable organic resource (SEO, blog, etc.). In that case, the App Store Optimization (ASO) and advertisements are the only things that this company can do for a controlled promotion.  &lt;/p&gt;

&lt;p&gt;We can divide services that are useful for development and promotion into six categories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Traffic attribution&lt;/strong&gt;. This will help answer where (from which ads) users come from and build the unit economy.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Product analytics&lt;/strong&gt;. This will help to collect user events and profiles and calculate any metric, including sales funnels.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Sending push notifications&lt;/strong&gt;. Optional: email.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;ASO-tools&lt;/strong&gt;. Analytics and optimization for organic growth in the App Store.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Crashlytics.&lt;/strong&gt; Application crash analytics.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s figure out what services are on the market, how much they cost, and how you should implement them. I will indicate only those services that I used myself or my friends did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attribution
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GZYXtEPS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://assets.website-files.com/5fcd41506ada3883d9750822/606ed6863b853206d60e80e9_21640be7fcbef78c7bf2a01d403e2e84.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GZYXtEPS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://assets.website-files.com/5fcd41506ada3883d9750822/606ed6863b853206d60e80e9_21640be7fcbef78c7bf2a01d403e2e84.png" alt="attribution checklist"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;‍&lt;/p&gt;

&lt;h3&gt;
  
  
  Why you need it
&lt;/h3&gt;

&lt;p&gt;Attribution binds users with traffic channels, determining which traffic channel—an advertising campaign or even a specific banner—brought a particular user.  &lt;/p&gt;

&lt;p&gt;Attribution has traditionally been the main component in purchasing paid traffic. Since most of the ads for apps are purchased from Facebook, you need a service to attribute this traffic. Unfortunately, there are few such services, and they are quite expensive. The reason is simple: it’s challenging to become a Facebook Marketing Partner.&lt;/p&gt;

&lt;h3&gt;
  
  
  Whom to choose?
&lt;/h3&gt;

&lt;p&gt;Several prominent players in the market know-how to attribute Facebook: AppsFlyer (responsible for 70% of the market), Adjust, Tenjin, and Branch.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much?
&lt;/h3&gt;

&lt;p&gt;Almost every player hides the price tags, but empirically you can find out that everyone is pretty equal  &lt;a href="https://www.appsflyer.com/pricing/"&gt;to AppsFlyer&lt;/a&gt;, and the average price will be 6 cents for the attribution of paid traffic. That is, if the service for a specific user gives you non-organic attribution, then you have to pay for it. Usually, organic traffic doesn't contribute to the cost.  &lt;/p&gt;

&lt;p&gt;In general, this price is unholy concerning the provided utility; therefore, the service’s cost will grow more slowly with the growth of volumes.  &lt;em&gt;Moreover, you will be obliged to pay even if the traffic is not converted and does not pay off, so you pay for the installation itself. This cost can quickly increase your cost per install (CPI) by 10%&lt;/em&gt;.  &lt;/p&gt;

&lt;p&gt;The conclusion is simple: bargain if you want to save money.  &lt;/p&gt;

&lt;p&gt;It's easy to calculate approximately how much AppsFlyer will cost. Let's say you buy $10,000 worth of traffic per month at an installation cost of $2 with 5,000 attributions -&amp;gt; 5,000 * 0.06 = $300.&lt;/p&gt;

&lt;p&gt;Note that if you don't receive attribution data when access to IDFA is disabled, you won't pay for attribution.  &lt;strong&gt;I officially asked AppsFlyer about this, here's the answer:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Yes, if the client has LAT enabled, we don’t receive IDFA/GAID, and the installation can convert to organic. But we will also try to do attribution through probabilistic modeling. And if that doesn’t work out, then it will go into organic production”.  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Product analytics
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mMjtjb1G--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://assets.website-files.com/5fcd41506ada3883d9750822/606ed84af4296e309b9c8048_f351995d655cbfbaab40562e970c0311.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mMjtjb1G--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://assets.website-files.com/5fcd41506ada3883d9750822/606ed84af4296e309b9c8048_f351995d655cbfbaab40562e970c0311.png" alt="product analytics checklist"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why you need it
&lt;/h3&gt;

&lt;p&gt;Product analytics is the ability to study what your users are doing in an application analytically. Simply put, it answers the question "What features are more users using?". Answers may vary, including cohort analysis and segmentation.  &lt;/p&gt;

&lt;p&gt;Any analytics system consists of two parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Data storage. This is where raw events about user actions are stored.&lt;/li&gt;
&lt;li&gt; BI or data visualization, building reports on top of raw data. This is what the user is working with.
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Whom to choose
&lt;/h3&gt;

&lt;p&gt;Unlike attribution, where there is no alternative to 3rd-party services, you can use open source solutions and create in-house solutions in product analytics.  &lt;/p&gt;

&lt;p&gt;Stack is traditionally created on a relational columnar database, for example, Clickhouse + Tableau for BI. Additionally, I recommend using Cube.js to collect data. As words for googling, I throw in: AWS Lambda, Redash, Google Big Query, Serverless.  &lt;/p&gt;

&lt;p&gt;In general, your own solution is an excellent way to go, but it is challenging to make it work, even with all its seeming simplicity. You will always want something extra. It will take development and support time while achieving good performance is also not a trivial task. The worst thing is that you can miss the error in the data and draw the wrong conclusions. In short, if you can assemble a data team, then this should be your option.  &lt;/p&gt;

&lt;p&gt;Amplitude and Mixpanel are prominent players in 3rd-party solutions. App Metrica and Firebase Analytics (Google Analytics for mobile devices) can be added here.  &lt;/p&gt;

&lt;p&gt;Usually, multiple analysts are selected to compare their accuracy, often taking free and paid ones.  &lt;/p&gt;

&lt;p&gt;The development and implementation of any analytics system starts with drawing up a map/list of events you want to track. After two weeks of hard work on the event table, you get something like this:  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BG8-HUy9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://assets.website-files.com/5fcd41506ada3883d9750822/607069baa5a20ce0e0ac17a6_2021-04-09%2520at%252017.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BG8-HUy9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://assets.website-files.com/5fcd41506ada3883d9750822/607069baa5a20ce0e0ac17a6_2021-04-09%2520at%252017.jpeg" alt="events table"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Which is later added to the application and tested.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much?
&lt;/h3&gt;

&lt;p&gt;It’s difficult to calculate the cost of your implementation. For the assessment, take the work of three people over a period of 4-6 months.  &lt;/p&gt;

&lt;p&gt;Let's estimate for services:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  App Metrica and Firebase Analytics are free.&lt;/li&gt;
&lt;li&gt;  Amplitude: Free for up to 10M events per month.&lt;/li&gt;
&lt;li&gt;  Mixpanel: Free for up to 100K unique users per month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I know many companies do the following: first, they track everything, and when it gets expensive, they remove unnecessary events, leaving a bare minimum.&lt;/p&gt;

&lt;p&gt;All analytics services make money on large companies, which is why they give such large free limits.  &lt;/p&gt;

&lt;p&gt;In my experience, usually, a paid plan starts at $2,000 per month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sending push notifications
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EDAKQ5FP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://assets.website-files.com/5fcd41506ada3883d9750822/606ed8871ea74183b9f524a6_d53d6b539814fde9c29e7807275bc439.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EDAKQ5FP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://assets.website-files.com/5fcd41506ada3883d9750822/606ed8871ea74183b9f524a6_d53d6b539814fde9c29e7807275bc439.png" alt="typical product push notification"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do you need it
&lt;/h3&gt;

&lt;p&gt;There are two purposes for sending push notifications:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Product. For example, you received a notification that your taxi is waiting.&lt;/li&gt;
&lt;li&gt; Marketing. When you are trying to sell something to a user.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Moreover, push notifications can be divided by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Triggered and caused manually&lt;/li&gt;
&lt;li&gt;  Targeted and not targeted.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of the product pushes are triggered, that is, when a trigger occurs due to user actions, then all events in product analytics are taken from the table above in product analytics.  &lt;/p&gt;

&lt;p&gt;Marketing push notifications can be triggered—for example, a user has unsubscribed, and we immediately give him a discount. They can also be manual: for example, we will send a notification to all our London users about a discount on products in an offline store.&lt;/p&gt;

&lt;h3&gt;
  
  
  Whom to choose
&lt;/h3&gt;

&lt;p&gt;Just like analytics, there is an option to make your own solution or use ready-to-go services. Both work through APNS (Apple Push Notifications Service).  &lt;/p&gt;

&lt;p&gt;Like many things in the Apple ecosystem for developers, working directly with APNS isn’t exactly easy and convenient. There are both technical tasks (push queue, sending speed, etc.) and purely product ones: you will have to match the user and his push token yourself.  &lt;/p&gt;

&lt;p&gt;The usual scheme is to use a provider to send push notifications and access them from your server through a convenient SDK/API. Examples of such services are AWS SNS and Firebase Cloud Messaging. FCM has a common Android system and shipping and delivery analytics, which is a nice addition.  &lt;/p&gt;

&lt;p&gt;The best system for sending push notifications allows you to build mailing campaigns, where you specify push chains, triggers, send delays, audiences, etc. These are complex and not cheap systems such as Intercom, Push Woosh, OneSignal. They support not only push notifications, but also email, in-app messages, and much more.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://firebase.google.com/products/cloud-messaging"&gt;Firebase Cloud Messaging&lt;/a&gt;  is free.&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://aws.amazon.com/sns/pricing/"&gt;AWS SNS&lt;/a&gt;: 1M free, then $0.5 per million.&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://onesignal.com/pricing"&gt;OneSignal&lt;/a&gt;: as many messages as you want, but 10K subscribers.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The price is growing quickly and strangely, so if you have 100K or more MAU, I’d be preparing for a price tag of $1,000 per month.&lt;/p&gt;

&lt;h2&gt;
  
  
  ASO
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mcPN3DFR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://assets.website-files.com/5fcd41506ada3883d9750822/606ed965aaf74277c4310f9b_d0360428d84087dba1ea5412f2b893ff.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mcPN3DFR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://assets.website-files.com/5fcd41506ada3883d9750822/606ed965aaf74277c4310f9b_d0360428d84087dba1ea5412f2b893ff.png" alt="ASO checklist"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;‍&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do you need it
&lt;/h3&gt;

&lt;p&gt;ASO (App Store Optimization) is an analog of SEO, only for applications. That is when a user searches for something in a search in the App Store or Google Play. With ASO you can:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Track keyword rankings&lt;/li&gt;
&lt;li&gt;  Track competitors&lt;/li&gt;
&lt;li&gt;  Upgrade ranking, including localization
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And other related tasks.  &lt;/p&gt;

&lt;p&gt;There are  &lt;a href="https://developer.apple.com/app-store/search/"&gt;Apple guidelines&lt;/a&gt;  for better ranking. Overall, ASO and SEO are similar things that need to be worked on all the time. You can find other guides and recommendations, they are publicly available.&lt;/p&gt;

&lt;h3&gt;
  
  
  Whom to choose
&lt;/h3&gt;

&lt;p&gt;I’m not an expert in ASO, but I heard good reviews about AppFollow and AsoDesk. Alternatively, you can write a self-written solution: there are many libraries on GitHub that will help with this, for example,  &lt;a href="https://github.com/facundoolano/aso"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much?
&lt;/h3&gt;

&lt;p&gt;Service price tags don’t vary much, I would aim for a few hundred dollars a month for a small application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Application crash analytics
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why do you need it
&lt;/h3&gt;

&lt;p&gt;Crash analytics helps you understand the reasons behind your app crashing—that is, it stopped working and the user was thrown out of it. In fact, this is the basic functionality that almost all developers use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Whom to choose
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://appmetrica.yandex.ru/docs/mobile-reports/concepts/crash-dump-errors.html"&gt;AppMetrica&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://firebase.google.com/products/crashlytics"&gt;Firebase&lt;/a&gt;  is the de facto industry standard.  &lt;/p&gt;

&lt;p&gt;Choose whatever suits you.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much?
&lt;/h3&gt;

&lt;p&gt;Free.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Where Adapty fits in?
&lt;/h2&gt;

&lt;p&gt;There is a sixth component your app would need: handling of payments and subscriptions. These things are tough to implement, especially when you want to export your data to 3rd-party attribution and analytics services.  &lt;/p&gt;

&lt;p&gt;Creating such systems on your own will take up to 6 months of work for a team of four. We don't think you need to do any of that, as Adapty will do it for you instead.  &lt;a href="https://app.adapty.io/registration"&gt;Try Adapty for free&lt;/a&gt;  with a 14-days trial.w&lt;/p&gt;

</description>
      <category>ios</category>
      <category>android</category>
      <category>analytics</category>
      <category>atribution</category>
    </item>
    <item>
      <title>How to create and configure in-app subscriptions on Android</title>
      <dc:creator>Vitaly Davydov</dc:creator>
      <pubDate>Thu, 28 Jan 2021 09:53:01 +0000</pubDate>
      <link>https://dev.to/iwitaly/how-to-create-and-configure-in-app-subscriptions-on-android-kpn</link>
      <guid>https://dev.to/iwitaly/how-to-create-and-configure-in-app-subscriptions-on-android-kpn</guid>
      <description>&lt;p&gt;The subscription industry is booming. Since 2016, Google has been incentivizing subscriptions, introducing subscription-only measures like &lt;a href="https://support.google.com/googleplay/android-developer/answer/112622?hl=en"&gt;halving Google Play sales cut&lt;/a&gt; when the user is subscribed for a year or longer. Today, this bet seems victorious: according to a recent study by AppAnnie, apps with subscriptions &lt;a href="https://www.appannie.com/en/go/state-of-mobile-2020/"&gt;account for 96%&lt;/a&gt; of consumer spend in top non-gaming apps.&lt;/p&gt;

&lt;p&gt;In this article, we are going the full distance in introducing subscriptions for an Android app. Along the way, we will examine every tool Google has to offer while also coming up with caveats that official guidelines left uncovered. Let’s assume that you have already paid for your &lt;a href="https://support.google.com/googleplay/android-developer/answer/7161426?hl=en"&gt;Google Play merchant account&lt;/a&gt; and have an  app, or draft for an app, available in your Google Play Console.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/Jg8G4ve9HRSpO/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/Jg8G4ve9HRSpO/giphy.gif" alt="Implementing in-app subscriptions"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a subscription from scratch
&lt;/h2&gt;

&lt;p&gt;To create a subscription, you will need to start with your &lt;a href="https://play.google.com/apps/publish"&gt;app page&lt;/a&gt; in Play Console, redirecting you to "All Apps". Select your app, go to section &lt;em&gt;Monetize –&amp;gt; Subscriptions&lt;/em&gt;, and click on &lt;em&gt;Create subscription button&lt;/em&gt; on the right.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--V-mpHB_L--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://lh6.googleusercontent.com/_S_LdP89PnrpMFdgy55T47S3wpvqUMc5Db8iMbAbOjx-9ZHSq1MgM21v2o7ctbBPKZ5Snbmtqz2Zwse1w5ck45hNz4Ak3PNCihORxqr_0p0itj0PgG0bKFdah1UnhnpEqaI60GH-" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--V-mpHB_L--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://lh6.googleusercontent.com/_S_LdP89PnrpMFdgy55T47S3wpvqUMc5Db8iMbAbOjx-9ZHSq1MgM21v2o7ctbBPKZ5Snbmtqz2Zwse1w5ck45hNz4Ak3PNCihORxqr_0p0itj0PgG0bKFdah1UnhnpEqaI60GH-" alt="Subscription section in Play Console"&gt;&lt;/a&gt;&lt;em&gt;Last step: choose "Create subscription" on the right&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Google will immediately ask you to provide a Product ID, which it uses to uniquely identify this particular subscription product. Every product available for purchase on Android has its Product ID. In this particular case, it will be an ID for your subscription. These IDs are not public (unlike Subscription Title), so don’t spend too much time here: we suggest you use an ID that’s easy to identify by you, and contains details about the product. However, remember that you can’t modify this and you can’t reuse a product ID inside the same app.&lt;/p&gt;

&lt;p&gt;Product ID can contain numbers, lowercase letters, underscores, and periods. You can’t name a Product ID starting with ‘android.test’, and you can’t change as well as reuse a Product ID once the product has been created.&lt;/p&gt;

&lt;p&gt;All the rest is to be filled in your local language. If you want to use different localizations, click on ‘Manage translations’ and fill in data for each locale. &lt;strong&gt;These are the following fields, all of which will be shown on the system dialog screen at the moment of purchase:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Title. Short name for an item, better to use titles up to 25 characters&lt;/li&gt;
&lt;li&gt;Description. Item description, up to 80 characters&lt;/li&gt;
&lt;li&gt;Default price and billing period. The latter may be weekly, every 4 weeks, monthly (yes, alongside 4 weeks option), every three months, half a year, or annually. Note that once you activated your subscription, you will not be able to change its billing period. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This policy by Google seems quite reasonable. It would be unfair if a user one day receives an email “Dear monthly subscriber, we’ve decided you will be charged annually starting next month, please be ready or cancel''. &lt;/p&gt;

&lt;p&gt;Simultaneously, if developers want to update their pricing, existing subscribers will be notified of the price change by email and through a notification on Google Play 7 days after the price change occurs. Subscribers will then have 30 days to agree to the price change; otherwise, their subscription will be cancelled on their next renewal date.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Benefits (optional). You may provide up to four benefits, which will describe the subscription features, up to 40 characters each.&lt;/li&gt;
&lt;li&gt;Benefits should highlight the features to give users a better idea of what your subscription offers, like “Full catalogue of TV shows and movies.”&lt;/li&gt;
&lt;li&gt;You can’t mention free trial or promotional price since not all users are eligible for it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We didn’t manage to find any mention by Google where the user might see these benefits, so we did our own investigation. Benefits are shown as the last resort when a user tries to cancel a subscription, on the very last system transactional page before the cancellation. Interestingly, neither benefits nor even app description is displayed when a user tries to purchase your plan – there is a title only.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XL8zjs9f--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/4tvi8y2dd4svbvc03mh3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XL8zjs9f--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/4tvi8y2dd4svbvc03mh3.png" alt="Cancel subscription pop-up in Google Play"&gt;&lt;/a&gt;&lt;em&gt;Only place your users will see these benefits&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional features of your subscription
&lt;/h2&gt;

&lt;p&gt;Google offers a range of instruments to enhance and tailor your marketing. You may choose not to use them, but we advise you to, as most of them are necessary for a pleasant user experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free trial
&lt;/h3&gt;

&lt;p&gt;Free trial lets your users try out a subscription before buying it. Free trials run for a period of time that you set, and then they automatically convert to a full subscription using the subscription’s time period and price.  By default, users can only receive one free trial across all available subscriptions on your app, but this can be changed in the Console settings.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--xb5LbTdZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://lh5.googleusercontent.com/eTqkrl0ikXQ8xIbX17CZbS2YY1NPCzuWnhjWP4hSCvfKNZ79WSx4pSZFqYrZHdO5BoF1x3zNESSrQRRUUJWN005eNYhZjfItDh86mikLnumeL_AuUvQK-UpYcoC1ygkgdbdRr1f_" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xb5LbTdZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://lh5.googleusercontent.com/eTqkrl0ikXQ8xIbX17CZbS2YY1NPCzuWnhjWP4hSCvfKNZ79WSx4pSZFqYrZHdO5BoF1x3zNESSrQRRUUJWN005eNYhZjfItDh86mikLnumeL_AuUvQK-UpYcoC1ygkgdbdRr1f_" alt="Subscriptions in Google Play Console"&gt;&lt;/a&gt;&lt;em&gt;Free trial settings are available under “Manage subscriptions settings” button&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Requirements
&lt;/h4&gt;

&lt;p&gt;You must let users know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what content or services they will be able to access with a free trial,&lt;/li&gt;
&lt;li&gt;how and when a free trial will convert to a paid subscription,&lt;/li&gt;
&lt;li&gt;how much the paid subscription will cost,&lt;/li&gt;
&lt;li&gt;and how to cancel if they do not want to convert to a paid subscription.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---GP2G5DK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/eowcvgc9vnloqz93m03n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---GP2G5DK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/eowcvgc9vnloqz93m03n.png" alt="Play Console Help section violations"&gt;&lt;/a&gt;&lt;em&gt;Examples of common violations, available on Play Console Help section&lt;/em&gt;)&lt;/p&gt;

&lt;p&gt;You can create one free trial per subscription product. Free trials are always $0. Trial periods must be 3 days or longer; you can change the trial period at any time, but only new subscribers will use the updated trial period. Users can only get a free trial if they haven’t previously purchased the subscription it applies to. For more information and examples, see &lt;a href="https://play.google.com/about/monetization-ads/subscriptions/#!?zippy_activeEl=free-trials#free-trials"&gt;Free Trials &amp;amp; Introductory Offers&lt;/a&gt; in the Developer Policy Center.&lt;/p&gt;

&lt;h3&gt;
  
  
  How users start a free trial
&lt;/h3&gt;

&lt;p&gt;To start a free trial, a user must complete the standard process for purchasing a subscription on Google Play. They aren’t charged at first, and they’re notified by email that the subscription includes a free trial period. Google Play records a transaction of $0.00, and the subscription is marked as purchased for the trial period or until it is cancelled.&lt;/p&gt;

&lt;p&gt;Now comes the tricky part: Google claims that the user is charged the day after the trial period ends, which is not entirely correct. Based on our experience, the user receives a transaction notification from his bank **the day before the trial ends. **Basically, Google allows the user to cancel and get an automatic refund, with the subscription also being cancelled the next day. If the user manages to cancel during the last day of the trial, the money will return without any additional user actions.&lt;/p&gt;

&lt;p&gt;We don’t really know whether Google charges users in advance or just pre-authorizes the necessary sum. Either way, this is usually confusing for your trial users as banks often don’t differentiate between these operations, resulting in angry letters to your customer support to get the refunds.&lt;/p&gt;

&lt;p&gt;When the trial ends, a user’s payment method is charged for the specified subscription amount (price can be either full, discounted, or an introductory offer), which recurs using the subscription’s set time period. The payment status may display as pending for up to 24 hours, even if no &lt;a href="https://support.google.com/googleplay/android-developer/answer/140504?hl=en#grace"&gt;grace period&lt;/a&gt; is set.&lt;/p&gt;

&lt;h3&gt;
  
  
  Introductory price
&lt;/h3&gt;

&lt;p&gt;With introductory pricing, you can specify an initial price that applies to a set number of days, weeks, months, or billing periods. For example, you can offer a subscription for $1 per month for the first three months. Or, you can provide an introductory price of $1 for 10 days, followed by a regular monthly price. At the end of the introductory period, users are charged the full subscription price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requirements&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You must clearly and accurately describe your offer’s terms, including the duration, pricing, and description of accessible content or services.&lt;/li&gt;
&lt;li&gt;Be clear about what happens when the introductory offer ends, including how much users will be charged and how they can cancel.&lt;/li&gt;
&lt;li&gt;The minimum introductory period is 3 days, and the maximum introductory period is 12 months.&lt;/li&gt;
&lt;li&gt;Introductory prices must be within the &lt;a href="https://support.google.com/googleplay/android-developer/table/3541286"&gt;accepted price range&lt;/a&gt; and less than the subscription's full price.&lt;/li&gt;
&lt;li&gt;If the introductory period is a different length of time than the subscription period, the introductory price must cost less per day than the original price. For example, if a subscription costs $15 per month (or $0.50 per day), a week-long introductory price must cost less than $3.50. For these calculations, a month is always considered to be 30 days.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also, two points to keep in mind. First, if you're offering a free trial and introductory price, your users are charged the introductory price at the end of the trial. Second, a user can only receive an introductory price for a specific subscription product (SKU) at one time.&lt;/p&gt;

&lt;p&gt;For more information and examples, see &lt;a href="https://play.google.com/about/monetization-ads/subscriptions/#!?zippy_activeEl=free-trials#free-trials"&gt;Free Trials &amp;amp; Introductory Offers&lt;/a&gt; in the Developer Policy Center.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You have two options to add introductory price: as a single payment, or a recurring payment.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Having chosen ‘single payment’, you can set up an introductory period of n days/weeks/months (within limits specified above) for a specific price regardless of your subscription’s original billing period. ‘Recurring payment’ is tied to the original billing period. You can only choose the number of introductory periods and introductory price itself; for example, 3 months for a monthly subscription, and a monthly price for these 3 months.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YpPKSX4x--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://lh5.googleusercontent.com/5oVD11U1KqaRW5VpWMUkTXzC9nxv7Pez3_ScfUxNamUCDA-apmFHgBTX63spsSKAlOdr5iOF0I_puXGB9aE2WpOsAn9BAGWAaRBpeBl-CC-f5N_SUQbdKDQph29wEi_mHmbIohSu" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YpPKSX4x--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://lh5.googleusercontent.com/5oVD11U1KqaRW5VpWMUkTXzC9nxv7Pez3_ScfUxNamUCDA-apmFHgBTX63spsSKAlOdr5iOF0I_puXGB9aE2WpOsAn9BAGWAaRBpeBl-CC-f5N_SUQbdKDQph29wEi_mHmbIohSu" alt="&amp;lt;img src=&amp;amp;quot;spacer.gif&amp;amp;quot; alt=&amp;amp;quot;&amp;amp;quot;&amp;gt;"&gt;&lt;/a&gt;Introductory pricing options in Play Console&lt;/p&gt;

&lt;h3&gt;
  
  
  Grace Period
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://developer.android.com/google/play/billing/billing_subscriptions#user-is-in-a-grace-period---subscription_in_grace_period"&gt;Grace period allows&lt;/a&gt; your subscribers to update their payment method if a recurring payment is declined. This can be useful if your subscribers have an expired credit card, subscribed using a prepaid card, or cancelled a card without updating their payment information. Using grace period is usually considered more humane towards your users, and there’s no reason to turn it off.&lt;/p&gt;

&lt;p&gt;At the start of the grace period, your subscribers will receive an email notifying them of a declined payment. They'll have time to update their payment method without interrupting their subscription. Once your subscribers update their payment method to a valid payment form, their next subscription billing date stays the same. If your subscriber's payment method is still declined by the end of the grace period, their subscription is cancelled, and they lose access to their subscription content.&lt;/p&gt;

&lt;p&gt;Grace period could last for 3, 7, 14, 30 days, or none. If your subscription is ‘every 4 weeks’, the maximum grace period is 14 days; for ‘weekly’ subscription the maximum is 7 days.&lt;/p&gt;

&lt;p&gt;For new subscription products, the following grace periods are set by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Weekly subscriptions: 3 days&lt;/li&gt;
&lt;li&gt;Monthly and ‘every 4 weeks’ subscriptions: 7 days&lt;/li&gt;
&lt;li&gt;Other subscription periods: 14 days&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Resubscribe
&lt;/h3&gt;

&lt;p&gt;Google &lt;a href="https://developer.android.com/google/play/billing/subscriptions#restore"&gt;offers&lt;/a&gt; the option to restore an expired subscription not through the app itself, but through the Play Market. If “Enabled for all” (which is the default value), users can resubscribe to the same SKU for up to a year after expiration by clicking Resubscribe in the Google Play subscription center, which generates a new subscription and purchase token.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--56pnR1W9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/glycna8s5vlfs08dzfhy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--56pnR1W9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/glycna8s5vlfs08dzfhy.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;em&gt;Subscription section of Google Play&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pause and Free trial limit
&lt;/h3&gt;

&lt;p&gt;When you click ‘Manage subscription settings’ button on the ‘Subscriptions’ page, you will see the following menu:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lJwyZEv---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://lh5.googleusercontent.com/Yz4FA2VgcJPOY1s4fVyVbYRvZF6M_YQh8uqGvKPa2LkcgjtjYiO321lBuT-lityMk3TipJVDGVuBzQQVyjWMddDxM-WG4v77Y5zf3n3UN5mE0aSnr8zcoRO48-RReWWZboOtQOfH" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lJwyZEv---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://lh5.googleusercontent.com/Yz4FA2VgcJPOY1s4fVyVbYRvZF6M_YQh8uqGvKPa2LkcgjtjYiO321lBuT-lityMk3TipJVDGVuBzQQVyjWMddDxM-WG4v77Y5zf3n3UN5mE0aSnr8zcoRO48-RReWWZboOtQOfH" alt="Subscriptions page in the Play Console"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Pause&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The default value is ‘enabled’. &lt;/p&gt;

&lt;p&gt;You can prevent voluntary churn by enabling users to pause their subscription. When you enable the pause feature, users can choose to pause their subscription for a period of time between one week and three months, depending on the recurring period. Once enabled, the pause option surfaces both in the subscription center and in the cancellation flow. &lt;/p&gt;

&lt;p&gt;Note that annual subscriptions cannot be paused, and pause limits of one week and three months are subject to change at any time. The subscription pause takes effect only after the current billing period ends. While the subscription is paused, the user doesn't have access to the subscription. &lt;/p&gt;

&lt;p&gt;At the end of the pause period, the subscription resumes, and Google attempts to renew the subscription. Once the resume is successful, the subscription will become active again. If the resume fails due to a payment issue, the user enters the account hold state. Users can also choose to manually resume a subscription at any time during the pause period. When a user resumes manually, the billing date changes to the manual resume date.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9gSlsXt4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://lh4.googleusercontent.com/DhY9Sik0ro2uQJhN2Gq2DHReC8ZqZqv7DwFdC3Lv_G5AANxx0s8Gl_02W_WMpbcIVn9AfEn9pF0XaHgDWq4zdm17h-IgUNzBOOosx7cb1GgkRL7oYJo2KuMSNTMyhzAEoOUGMf1P" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9gSlsXt4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://lh4.googleusercontent.com/DhY9Sik0ro2uQJhN2Gq2DHReC8ZqZqv7DwFdC3Lv_G5AANxx0s8Gl_02W_WMpbcIVn9AfEn9pF0XaHgDWq4zdm17h-IgUNzBOOosx7cb1GgkRL7oYJo2KuMSNTMyhzAEoOUGMf1P" alt="User pauses and then resumes subscription timeline from Google"&gt;&lt;/a&gt;&lt;em&gt;Courtesy of Google’s &lt;a href="https://developer.android.com/google/play/billing/subscriptions#pause"&gt;Android Developer&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;Free trial limit&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You have two options - ‘One per subscription’ or ‘One across all subscriptions’.&lt;/p&gt;

&lt;p&gt;Overall, every field, except &lt;strong&gt;product ID&lt;/strong&gt; and &lt;strong&gt;billing period&lt;/strong&gt;, can be edited at any time. Billing period can be edited before you push the ‘Activate’ button. Once activated, the subscription product itself can’t be deleted.&lt;/p&gt;

&lt;p&gt;To make your subscription visible to your testers, you need to activate it first. “Activate” button will become available once you fill in all the necessary fields and save changes, meaning your new clients can purchase it from your app. Testers can work with subscriptions without paying if you add them to License testing list by going to &lt;a href="https://play.google.com/apps/publish"&gt;your app page&lt;/a&gt; –&amp;gt; Settings –&amp;gt; License testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google Play rules
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Subscription prices
&lt;/h3&gt;

&lt;p&gt;You can choose to make certain content or services in your app available to users at a regular rate for a particular period. Users will be charged your chosen rate at your selected frequency until they decide to cancel.&lt;/p&gt;

&lt;p&gt;For example, a user who buys a one-year subscription on Jan 1 for $10 will have access to the subscription until December 31 and will be charged $10 on the following renewal date.&lt;/p&gt;

&lt;h3&gt;
  
  
  Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;As a developer, you must be transparent about any subscription services or content you offer within your app.&lt;/li&gt;
&lt;li&gt;You must communicate your offer clearly in any in-app promotions or splash screens.&lt;/li&gt;
&lt;li&gt;You must be explicit about your offer terms, including the cost of your subscription, the frequency of your billing cycle, and whether a subscription is required to use the app. Users should not have to perform any additional action to review the information.&lt;/li&gt;
&lt;li&gt;Subscription pricing must be within the &lt;a href="https://support.google.com/googleplay/android-developer/table/3541286"&gt;accepted price range&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Your subscription title must accurately reflect your offer. For example, don’t name your subscription “Free Trial”.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Price changes
&lt;/h3&gt;

&lt;p&gt;After you change the price of an existing subscription, here’s how it affects new users and existing subscribers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New users can immediately see the subscription’s new price and subscribe to it on Google Play.&lt;/li&gt;
&lt;li&gt;Existing subscribers receiving a price increase will be notified of the price change by email and through a notification on Google Play 7 days after the price change occurs. Subscribers will then have 30 days to agree to the price change; otherwise, their subscription will be cancelled on their next renewal date.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, price increases may cause an increase in churn if subscribers do not accept the price change. We advise you to contact subscribers to inform them of the price increase in advance (Google recommends within a week) and give them a convincing justification. Customers will pay more if you give them a reason to do so&lt;/p&gt;

&lt;p&gt;If you change the price multiple times before the user's next renewal date, the user only has to respond to the most recent price change. Existing subscribers receiving a price decrease will be notified and receive a lower price on their next renewal date.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managing price changes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Once you change the subscription price, it can’t be reverted.&lt;/li&gt;
&lt;li&gt;You can change a subscription’s price multiple times, but that's not what Google recommends as you may lose some subscribers each time you change a price&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you change a subscription’s price twice in a short period of time, your subscribers will need to agree to the first price change, and then agree on the second price change for your intended price to take effect.&lt;/p&gt;

&lt;p&gt;If you want to offer one price to existing subscribers and a different price to new users, you can create a new subscription with the price you want to offer to new users. This way, your existing subscribers can continue renewing their subscriptions without having to agree to a price change.&lt;/p&gt;

&lt;p&gt;And that's it, you've created your subscription! Next, we will be covering how to handle it.&lt;/p&gt;

</description>
      <category>android</category>
      <category>subscriptions</category>
      <category>payments</category>
      <category>programming</category>
    </item>
    <item>
      <title>Webflow + Ghost blog with Caddy</title>
      <dc:creator>Vitaly Davydov</dc:creator>
      <pubDate>Mon, 14 Dec 2020 17:30:07 +0000</pubDate>
      <link>https://dev.to/iwitaly/webflow-ghost-blog-with-caddy-5b0e</link>
      <guid>https://dev.to/iwitaly/webflow-ghost-blog-with-caddy-5b0e</guid>
      <description>&lt;p&gt;Modern SaaS applications usually run a landing page, blog, and the main app separately. For a landing page, you may want to use Tilda, Webflow, or other web-builders. For a blog, it’s common to use self-hosted CMS such as WordPress, Ghost, or others.&lt;/p&gt;

&lt;p&gt;It’s crucially important to index the main domain &lt;em&gt;your_domain.com&lt;/em&gt; instead of a subdomain &lt;em&gt;blog.your_domain.com&lt;/em&gt; for SEO purposes. You want as many links to your main domain as possible. The more content on &lt;em&gt;your_domain.com/blog&lt;/em&gt;, the more Google will index it.&lt;/p&gt;

&lt;p&gt;Running Ghost blog on a subdomain such as &lt;em&gt;blog.your_domain.com&lt;/em&gt; is easy, just create a new A-record in your DNS provider and point it to a machine with running Ghost instance. If you want to run &lt;em&gt;your_domain.com&lt;/em&gt; on a Webflow and &lt;em&gt;your_domain.com/blog&lt;/em&gt; being self-hosted Ghost you need a &lt;a href="https://en.wikipedia.org/wiki/Reverse_proxy"&gt;reverse-proxy&lt;/a&gt; server.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--b3ulfkdi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/vdfbeyms9kpk3vfyaj4x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--b3ulfkdi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/vdfbeyms9kpk3vfyaj4x.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;br&gt;
Reverse proxy server&lt;br&gt;
From a devops point of view our goals are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;your_domain.com&lt;/em&gt; -&amp;gt; Webflow&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;your_domain.com/&lt;/em&gt;* -&amp;gt; Webflow&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;your_domain.com/blog&lt;/em&gt; -&amp;gt; self hosted Ghost blog&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;blog.your_domain.com&lt;/em&gt; -&amp;gt; &lt;em&gt;your_domain.com/blog&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We’re going to use &lt;a href="https://caddyserver.com/"&gt;Caddy&lt;/a&gt; for reverse-proxy. The reason is Docker-friendly config, super fast to deploy without deep knowledge of devops (hey, Nginx). Find the official Caddy image &lt;a href="https://hub.docker.com/_/caddy?tab=description"&gt;here&lt;/a&gt;. Caddy needs you to mount volumes for proper working.&lt;/p&gt;

&lt;p&gt;We’ll host Ghost and Caddy on the same machine and in a single &lt;em&gt;docker-compose.yml&lt;/em&gt; file.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: "3.7"

services:
 caddy:
   image: caddy:2
   restart: unless-stopped
   ports:
     - "80:80"
     - "443:443"
   volumes:
     - $PWD/Caddyfile:/etc/caddy/Caddyfile
     - $PWD/site:/srv
     - caddy_data:/data
     - caddy_config:/config
 ghost:
   image: ghost:3
   environment:
     NODE_ENV: production
     url: https://adapty.io/blog
   volumes:
     - ./blog:/var/lib/ghost/content
volumes:
 caddy_data:
 caddy_config:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now for &lt;em&gt;Caddyfile&lt;/em&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;blog.adapty.io {
   redir https://adapty.io/blog{uri} #point to subdirectory
}

adapty.io {
   redir /blog /blog/ #trailing slash
   reverse_proxy /blog/* ghost:2368 { #proxy to Ghost container
       header_up Host {host}
   }
   reverse_proxy proxy.webflow.com { #proxy to Webflow
       header_up Host {host}
   }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Run *docker-compose up -d *and here you go!&lt;/p&gt;

&lt;p&gt;By default, Caddy passes thru incoming headers to the backend—including the Host header—without modifications, with two exceptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It adds or augments the X-Forwarded-For header field.&lt;/li&gt;
&lt;li&gt;It sets the X-Forwarded-Proto header field.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read more in their docs &lt;a href="https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#headers"&gt;https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#headers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lastly,&lt;/p&gt;

&lt;p&gt;Point A record for the main domain to your IP.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--J4UBj4VR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/r9fjjtyasw7actmktha3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--J4UBj4VR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/r9fjjtyasw7actmktha3.png" alt="Alt Text"&gt;&lt;/a&gt;Change your DNS A record and point to a VM&lt;br&gt;
In Webflow turn off SSL proxy as Caddy will serve it for you automatically (very cool, yeah? Without a certbot).&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8l7NCYwD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/h6tditcopnggf7vorig9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8l7NCYwD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/h6tditcopnggf7vorig9.png" alt="Alt Text"&gt;&lt;/a&gt;Turn off SSL in Webflow as Caddy will create a certificate for you&lt;br&gt;
That’s it!&lt;/p&gt;

</description>
      <category>docker</category>
      <category>caddy</category>
      <category>webflow</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
