Verifying Apple In-App Subscription in Golang

Zco Corporation
Custom App Development
4 min readFeb 6, 2024

In-app subscriptions are a crucial part of monetizing mobile applications, and handling the verification of these subscriptions is essential for maintaining a secure and reliable system. The App Store updates in-app transactions immediately after completing subscriptions or after any changes that happen to the subscription, even when the app is not running. These may include.

  • Subscription is active.
  • Subscription expired.
  • Subscription is in a billing retry period.
  • Subscription is in a Billing Grace Period.
  • Subscription is revoked.

Prerequisites

  • Apple Appstore Privatekey in JSON format to use in JWT token in requests to Apple App Store server API’s.
  • Transaction ID from the application when they complete a subscription purchase. The ID is obtained via an API.

Code Overview

The verification process is broken down into several steps

  1. Creates a JWT token for authentication.
  2. Make an HTTP request to Apple’s server to retrieve the transaction information.
  3. Parses the response to evaluate the validity.

Creates a JWT token for authentication.
This token is essential for secure communication with Apple API services. Sample code to create this token is as follows:

func CreateAppStoreJWTToken(issuerID string, privateKeyPath string, keyID string) (string, error) {

// Read the private key file
privateKeyBytes, err := os.ReadFile(ApplePrivateKeyPath)
if err != nil {
return "", err
}

// Parse the private key using the PKCS#8 format
block, _ := pem.Decode(privateKeyBytes)
if block == nil {
return "", fmt.Errorf("failed to decode PEM block")
}

privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return "", err
}

ecdsaKey, ok := privateKey.(*ecdsa.PrivateKey)
if !ok {
return "", fmt.Errorf("invalid private key type")
}

// Define the token claims
claims := jwt.MapClaims{
"iss": issuerID,
"iat": time.Now().Unix(),
"exp": time.Now().Add(20 * time.Minute).Unix(),
"aud": "appstoreconnect-v1",
"bid": BundleID,
}

// Create the token
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)

// Set the header fields
token.Header["kid"] = keyID

// Sign the token
tokenString, err := token.SignedString(ecdsaKey)
if err != nil {
return "", err
}

return tokenString, nil
}

In the above sample code,

  • issuerID is your Apple Developer Team ID.
  • privateKeyPath refers to the path to the private key file used for signing the JWT token.
  • keyID refers to the key identifier (kid) associated with the private key. You can find the key ID in your App Store Connect account under Settings > Keys.
  • BundleID is your app’s bundle ID usually in the format com.yourcompany.appname.

Make an HTTP request to Apple’s server to retrieve the transaction information.
Once we have the token, we can make an HTTP request to Apple’s server using the JWT token to retrieve the transaction information.

    // Parse the endpoint URL
u, err := url.Parse(endpoint)
if err != nil {
return nil, err
}

// Add query parameter
q := u.Query()
q.Add("productType", "AUTO_RENEWABLE")
u.RawQuery = q.Encode()

// Create HTTP client and request
client := &http.Client{}
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+jwtToken)

// Send request and handle response
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to retrieve data. Status code: %d", resp.StatusCode)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

return body, nil
}

In the above code, the endpoint is the URL to Apple’s API https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/ for production and https://api.storekit-sandbox.itunes.apple.com/inApps/v1/subscriptions/ for the sandbox.

jwtToken is the token we created in step 1 above.

The line q.Add(“productType”, “AUTO_RENEWABLE”)is adding a query parameter to the URL constructed. This indicates that the product being queried or retrieved from the App Store server is an auto-renewable subscription.

If the response status code is not 200 (OK), it returns an error. Otherwise, it reads and returns the response body.

Parses the response to evaluate the validity.

func parseTransactionHistory(data []byte) {

// Define a struct to unmarshal the JSON data into
var response struct {
Environment string `json:"environment"`
BundleID string `json:"bundleId"`
Data []struct {
SubscriptionGroupIdentifier string `json:"subscriptionGroupIdentifier"`
LastTransactions []struct {
OriginalTransactionID string `json:"originalTransactionId"`
Status int `json:"status"`
SignedTransactionInfo string `json:"signedTransactionInfo"`
} `json:"lastTransactions"`
SignedRenewalInfo string `json:"signedRenewalInfo"`
} `json:"data"`
}

// Unmarshal the JSON data into the 'response' struct
if err := json.Unmarshal(data, &response); err != nil {
return nil, 0, err
}
// Process the response here for validating the transaction.
// response is now an array of subscription related transactions.
}

The above function takes the data we retrieved from step 2 and unmarshal it to get the transactions array.

Each item in the response incorporates a signed transaction info and transaction status.

Transaction Status can be:

  • The auto-renewable subscription is active.
  • The auto-renewable subscription is expired.
  • The auto-renewable subscription is in a billing retry period.
  • The auto-renewable subscription is in a Billing Grace Period.
  • The auto-renewable subscription is revoked. The App Store refunded the transaction or revoked it from Family Sharing.

Any signed transaction will have details on PurchaseDate, ExpiresDate, transactionID etc. which can also be used along with transaction status to determine the validity of the transaction.

Conclusion

Verifying Apple in-app subscription purchases is crucial for maintaining a secure and reliable subscription system. The provided Golang code serves as a foundation for implementing this verification process within a server-side application. By integrating and adapting this code to your specific use case, you can ensure the authenticity and accuracy of Apple in-app purchases.

Work with Zco Corporation to develop your app: https://www.zco.com/mobile-app-development/

Written by our Senior Project Manager, Smith S Raj
https://www.linkedin.com/in/smithsraj/

--

--

Zco Corporation
Custom App Development
0 Followers

We are a custom software development company headquartered in Nashua, NH.