The Python server library for the App Store Server API and App Store Server Notifications. This library has been rewritten by Bahadır Araz to use asynchronous code. This update prevents blocking when handling concurrent API requests and includes instructions on how to use the library with Pydantic for improved data validation with modern Python practices.
- Python 3.7+
pip install app-store-server-library-python-asyncTo use the App Store Server API or create promotional offer signatures, a signing key downloaded from App Store Connect is required. To obtain this key, you must have the Admin role. Go to Users and Access > Integrations > In-App Purchase. Here you can create and manage keys, as well as find your issuer ID. When using a key, you'll need the key ID and issuer ID as well.
Download and store the root certificates found in the Apple Root Certificates section of the Apple PKI site. Provide these certificates as an array to a SignedDataVerifier to allow verifying the signed data comes from Apple.
fromappstoreserverlibraryasync.api_clientimportAsyncAppStoreServerAPIClient, APIExceptionfromappstoreserverlibraryasync.models.EnvironmentimportEnvironmentdefread_private_key(file_path):
withopen(file_path, "r") asfile:
returnfile.read()
private_key=read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8") # Implementation will varykey_id="ABCDEFGHIJ"issuer_id="99b16628-15e4-4668-972b-eeff55eeff55"bundle_id="com.example"environment=Environment.SANDBOXclient=AsyncAppStoreServerAPIClient(private_key, key_id, issuer_id, bundle_id, environment)
asyncdefmain():
try:
response=awaitclient.request_test_notification()
print(response)
exceptAPIExceptionase:
print(e)fromappstoreserverlibraryasync.models.EnvironmentimportEnvironmentfromappstoreserverlibraryasync.signed_data_verifierimportVerificationException, SignedDataVerifierdefload_root_certificates():
cert_paths= [
f"AppleComputerRootCertificate.cer",
f"AppleRootCA-G3.cer",
f"AppleRootCA-G2.cer",
f"AppleIncRootCertificate.cer",
]
certs= []
forpathincert_paths:
file=open(path, "rb")
cert=file.read()
file.close()
certs.append(cert)
returncertsasyncdefmain():
root_certificates=load_root_certificates()
enable_online_checks=Truebundle_id="com.example"environment=Environment.SANDBOXapp_apple_id=None# appAppleId must be provided for the Production environmentsigned_data_verifier=SignedDataVerifier(root_certificates, enable_online_checks, environment, bundle_id,
app_apple_id)
try:
signed_notification="ey.."payload=awaitsigned_data_verifier.verify_and_decode_notification(signed_notification)
print(payload)
exceptVerificationExceptionase:
print(e)fromappstoreserverlibraryasync.api_clientimportAsyncAppStoreServerAPIClient, APIExceptionfromappstoreserverlibraryasync.models.EnvironmentimportEnvironmentfromappstoreserverlibraryasync.receipt_utilityimportReceiptUtilityfromappstoreserverlibraryasync.models.HistoryResponseimportHistoryResponsefromappstoreserverlibraryasync.models.TransactionHistoryRequestimportTransactionHistoryRequest, ProductType, Orderdefread_private_key(file_path):
withopen(file_path, "r") asfile:
returnfile.read()
private_key=read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8") # Implementation will varykey_id="ABCDEFGHIJ"issuer_id="99b16628-15e4-4668-972b-eeff55eeff55"bundle_id="com.example"environment=Environment.SANDBOXclient=AsyncAppStoreServerAPIClient(private_key, key_id, issuer_id, bundle_id, environment)
receipt_util=ReceiptUtility()
app_receipt="MI.."asyncdefextract_transaction_receipts_from_id(app_receipt):
try:
transaction_id=receipt_util.extract_transaction_id_from_app_receipt(app_receipt)
iftransaction_id!=None:
transactions= []
response: HistoryResponse=Nonerequest: TransactionHistoryRequest=TransactionHistoryRequest(
sort=Order.ASCENDING,
revoked=False,
productTypes=[ProductType.AUTO_RENEWABLE]
)
whileresponseisNoneorresponse.hasMore:
revision=response.revisionifresponseisnotNoneelseNoneresponse=awaitclient.get_transaction_history(transaction_id, revision, request)
fortransactioninresponse.signedTransactions:
transactions.append(transaction)
print(transactions)
exceptAPIExceptionase:
print(e)fromappstoreserverlibraryasync.promotional_offerimportPromotionalOfferSignatureCreatorimporttimedefread_private_key(file_path):
withopen(file_path, "r") asfile:
returnfile.read()
private_key=read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8") # Implementation will varykey_id="ABCDEFGHIJ"bundle_id="com.example"promotion_code_signature_generator=PromotionalOfferSignatureCreator(private_key, key_id, bundle_id)
product_id="<product_id>"subscription_offer_id="<subscription_offer_id>"application_username="<application_username>"nonce="<nonce>"timestamp=round(time.time() *1000)
base64_encoded_signature=promotion_code_signature_generator.create_signature(product_id, subscription_offer_id,
application_username, nonce, timestamp)Apple has interestingly chosen to use attrs library. Here is an example of how you can convert it to Pydantic.
fromappstoreserverlibraryasync.models.JWSTransactionDecodedPayloadimportJWSTransactionDecodedPayload, Type,InAppOwnershipType, RevocationReason, OfferType, Environment, TransactionReason, OfferDiscountTypefrompydanticimportBaseModelimportattrfromtypingimportOptionalexample_payload=JWSTransactionDecodedPayload(
originalTransactionId="1000000077803123",
transactionId="1000000077803124",
webOrderLineItemId="1000000077803125",
bundleId="com.example.app",
productId="com.example.product",
subscriptionGroupIdentifier="12345678",
purchaseDate=1622548800000,
originalPurchaseDate=1622548800000,
expiresDate=1625130800000,
quantity=1,
type=Type("Auto-Renewable Subscription"),
rawType="1",
appAccountToken="E7DE6CBE-E0C2-4B52-A00C-E0C2E7DE6CBE",
inAppOwnershipType=InAppOwnershipType("PURCHASED"),
rawInAppOwnershipType="PURCHASED",
signedDate=1622548800000,
revocationReason=RevocationReason(1),
rawRevocationReason=0,
revocationDate=1622548800000,
isUpgraded=False,
offerType=OfferType(1),
rawOfferType=1,
offerIdentifier="offer123",
environment=Environment("Sandbox"),
rawEnvironment="PRODUCTION",
storefront="USA",
storefrontId="143441",
transactionReason=TransactionReason("PURCHASE"),
rawTransactionReason="PURCHASE",
currency="USD",
price=499,
offerDiscountType=OfferDiscountType("FREE_TRIAL"),
rawOfferDiscountType="FREE_TRIAL"
)
classPydanticJWSTransactionDecodedPayload(BaseModel):
originalTransactionId: Optional[str] =NonetransactionId: Optional[str] =NonewebOrderLineItemId: Optional[str] =NonebundleId: Optional[str] =NoneproductId: Optional[str] =NonesubscriptionGroupIdentifier: Optional[str] =NonepurchaseDate: Optional[int] =NoneoriginalPurchaseDate: Optional[int] =NoneexpiresDate: Optional[int] =Nonequantity: Optional[int] =Nonetype: Optional[str] =NonerawType: Optional[str] =NoneappAccountToken: Optional[str] =NoneinAppOwnershipType: Optional[str] =NonerawInAppOwnershipType: Optional[str] =NonesignedDate: Optional[int] =NonerevocationReason: Optional[int] =NonerawRevocationReason: Optional[int] =NonerevocationDate: Optional[int] =NoneisUpgraded: Optional[bool] =NoneofferType: Optional[int] =NonerawOfferType: Optional[int] =NoneofferIdentifier: Optional[str] =Noneenvironment: Optional[str] =NonerawEnvironment: Optional[str] =Nonestorefront: Optional[str] =NonestorefrontId: Optional[str] =NonetransactionReason: Optional[str] =NonerawTransactionReason: Optional[str] =Nonecurrency: Optional[str] =Noneprice: Optional[int] =NoneofferDiscountType: Optional[str] =NonerawOfferDiscountType: Optional[str] =None# Convert to Pydanticpydanctic_payload=PydanticJWSTransactionDecodedPayload(**attr.asdict(example_payload))
print(pydanctic_payload)Only the latest major version of the library will receive updates, including security updates. Therefore, it is recommended to update to new major versions.