├── tests ├── __init__.py ├── resources │ ├── models │ │ ├── apiUnknownError.json │ │ ├── transactionInfoResponse.json │ │ ├── apiException.json │ │ ├── appTransactionInfoResponse.json │ │ ├── apiTooManyRequestsException.json │ │ ├── invalidTransactionIdError.json │ │ ├── requestTestNotificationResponse.json │ │ ├── transactionIdNotFoundError.json │ │ ├── extendRenewalDateForAllActiveSubscribersResponse.json │ │ ├── appTransactionDoesNotExistError.json │ │ ├── lookupOrderIdResponse.json │ │ ├── invalidAppAccountTokenUUIDError.json │ │ ├── familyTransactionNotSupportedError.json │ │ ├── transactionIdNotOriginalTransactionId.json │ │ ├── extendSubscriptionRenewalDateResponse.json │ │ ├── getImageListResponse.json │ │ ├── getMessageListResponse.json │ │ ├── getRefundHistoryResponse.json │ │ ├── getStatusOfSubscriptionRenewalDateExtensionsResponse.json │ │ ├── transactionHistoryResponse.json │ │ ├── decodedRealtimeRequest.json │ │ ├── getTestNotificationStatusResponse.json │ │ ├── transactionHistoryResponseWithMalformedAppAppleId.json │ │ ├── transactionHistoryResponseWithMalformedEnvironment.json │ │ ├── signedExternalPurchaseTokenNotification.json │ │ ├── signedExternalPurchaseTokenSandboxNotification.json │ │ ├── signedNotification.json │ │ ├── appTransaction.json │ │ ├── signedConsumptionRequestNotification.json │ │ ├── signedSummaryNotification.json │ │ ├── getNotificationHistoryResponse.json │ │ ├── signedRenewalInfo.json │ │ ├── signedTransaction.json │ │ └── getAllSubscriptionStatusesResponse.json │ ├── certs │ │ ├── testCA.der │ │ └── testSigningKey.p8 │ ├── mock_signed_data │ │ ├── legacyTransaction │ │ ├── transactionInfo │ │ ├── renewalInfo │ │ ├── wrongBundleId │ │ ├── missingX5CHeaderClaim │ │ └── testNotification │ └── xcode │ │ ├── xcode-signed-renewal-info │ │ ├── xcode-signed-app-transaction │ │ ├── xcode-signed-transaction │ │ ├── xcode-app-receipt-empty │ │ └── xcode-app-receipt-with-transaction ├── test_promotional_offer_signature_creator.py ├── test_receipt_utility.py ├── util.py ├── test_payload_verification.py ├── test_xcode_signed_data.py └── test_jws_signature_creator.py ├── MANIFEST.in ├── appstoreserverlibrary ├── __init__.py ├── py.typed ├── models │ ├── __init__.py │ ├── AutoRenewStatus.py │ ├── ImageState.py │ ├── Platform.py │ ├── MessageState.py │ ├── OfferType.py │ ├── RevocationReason.py │ ├── OrderLookupStatus.py │ ├── PurchasePlatform.py │ ├── Status.py │ ├── UserStatus.py │ ├── InAppOwnershipType.py │ ├── RefundPreference.py │ ├── Environment.py │ ├── ExtendReasonCode.py │ ├── ConsumptionStatus.py │ ├── OfferDiscountType.py │ ├── PriceIncreaseStatus.py │ ├── ExpirationIntent.py │ ├── TransactionReason.py │ ├── Type.py │ ├── UpdateAppAccountTokenRequest.py │ ├── Message.py │ ├── ConsumptionRequestReason.py │ ├── RealtimeRequestBody.py │ ├── PlayTime.py │ ├── SendTestNotificationResponse.py │ ├── AppTransactionInfoResponse.py │ ├── DefaultConfigurationRequest.py │ ├── TransactionInfoResponse.py │ ├── ResponseBodyV2.py │ ├── AccountTenure.py │ ├── DeliveryStatus.py │ ├── GetImageListResponse.py │ ├── MassExtendRenewalDateResponse.py │ ├── GetMessageListResponse.py │ ├── UploadMessageImage.py │ ├── SendAttemptResult.py │ ├── FirstSendAttemptResult.py │ ├── AlternateProduct.py │ ├── LifetimeDollarsPurchased.py │ ├── LifetimeDollarsRefunded.py │ ├── Subtype.py │ ├── GetImageListResponseItem.py │ ├── GetMessageListResponseItem.py │ ├── CheckTestNotificationResponse.py │ ├── NotificationHistoryResponseItem.py │ ├── SubscriptionGroupIdentifierItem.py │ ├── OrderLookupResponse.py │ ├── NotificationTypeV2.py │ ├── ExtendRenewalDateRequest.py │ ├── RealtimeResponseBody.py │ ├── RefundHistoryResponse.py │ ├── UploadMessageRequestBody.py │ ├── PromotionalOffer.py │ ├── SendAttemptItem.py │ ├── NotificationHistoryResponse.py │ ├── ExtendRenewalDateResponse.py │ ├── ExternalPurchaseToken.py │ ├── PromotionalOfferSignatureV1.py │ ├── LastTransactionsItem.py │ ├── StatusResponse.py │ ├── MassExtendRenewalDateRequest.py │ ├── MassExtendRenewalDateStatusResponse.py │ ├── HistoryResponse.py │ ├── DecodedRealtimeRequestBody.py │ ├── Summary.py │ ├── NotificationHistoryRequest.py │ ├── Data.py │ ├── TransactionHistoryRequest.py │ ├── ResponseBodyV2DecodedPayload.py │ ├── AppTransaction.py │ ├── LibraryUtility.py │ ├── ConsumptionRequest.py │ └── JWSRenewalInfoDecodedPayload.py ├── promotional_offer.py ├── receipt_utility.py └── jws_signature_creator.py ├── docs └── requirements.txt ├── requirements.txt ├── .github ├── dependabot.yml └── workflows │ ├── ci-prb.yml │ ├── ci-release.yml │ ├── ci-snapshot.yml │ └── ci-release-docs.yml ├── .gitignore ├── setup.py ├── LICENSE.txt ├── CONTRIBUTING.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md -------------------------------------------------------------------------------- /appstoreserverlibrary/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /appstoreserverlibrary/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx == 8.2.3 -------------------------------------------------------------------------------- /appstoreserverlibrary/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/models/apiUnknownError.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 9990000, 3 | "errorMessage": "Testing error." 4 | } -------------------------------------------------------------------------------- /tests/resources/models/transactionInfoResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "signedTransactionInfo": "signed_transaction_info_value" 3 | } -------------------------------------------------------------------------------- /tests/resources/models/apiException.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 5000000, 3 | "errorMessage": "An unknown error occurred." 4 | } -------------------------------------------------------------------------------- /tests/resources/models/appTransactionInfoResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "signedAppTransactionInfo": "signed_app_transaction_info_value" 3 | } -------------------------------------------------------------------------------- /tests/resources/models/apiTooManyRequestsException.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 4290000, 3 | "errorMessage": "Rate limit exceeded." 4 | } -------------------------------------------------------------------------------- /tests/resources/models/invalidTransactionIdError.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 4000006, 3 | "errorMessage": "Invalid transaction id." 4 | } -------------------------------------------------------------------------------- /tests/resources/models/requestTestNotificationResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "testNotificationToken": "ce3af791-365e-4c60-841b-1674b43c1609" 3 | } -------------------------------------------------------------------------------- /tests/resources/certs/testCA.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/app-store-server-library-python/main/tests/resources/certs/testCA.der -------------------------------------------------------------------------------- /tests/resources/models/transactionIdNotFoundError.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 4040010, 3 | "errorMessage": "Transaction id not found." 4 | } -------------------------------------------------------------------------------- /tests/resources/mock_signed_data/legacyTransaction: -------------------------------------------------------------------------------- 1 | ewoicHVyY2hhc2UtaW5mbyIgPSAiZXdvaWRISmhibk5oWTNScGIyNHRhV1FpSUQwZ0lqTXpPVGt6TXprNUlqc0tmUW89IjsKfQo= -------------------------------------------------------------------------------- /tests/resources/models/extendRenewalDateForAllActiveSubscribersResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestIdentifier": "758883e8-151b-47b7-abd0-60c4d804c2f5" 3 | } -------------------------------------------------------------------------------- /tests/resources/models/appTransactionDoesNotExistError.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 4040019, 3 | "errorMessage": "No AppTransaction exists for the customer." 4 | } -------------------------------------------------------------------------------- /tests/resources/models/lookupOrderIdResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": 1, 3 | "signedTransactions": [ 4 | "signed_transaction_one", 5 | "signed_transaction_two" 6 | ] 7 | } -------------------------------------------------------------------------------- /tests/resources/models/invalidAppAccountTokenUUIDError.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 4000183, 3 | "errorMessage": "Invalid request. The app account token field must be a valid UUID." 4 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs >= 21.3.0 2 | PyJWT >= 2.6.0, < 3 3 | requests >= 2.28.0, < 3 4 | cryptography >= 40.0.0 5 | pyOpenSSL >= 23.1.1 6 | asn1==2.8.0 7 | cattrs >= 23.1.2 8 | httpx==0.28.1 9 | -------------------------------------------------------------------------------- /tests/resources/models/familyTransactionNotSupportedError.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 4000185, 3 | "errorMessage": "Invalid request. Family Sharing transactions aren't supported by this endpoint." 4 | } -------------------------------------------------------------------------------- /tests/resources/models/transactionIdNotOriginalTransactionId.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode": 4000187, 3 | "errorMessage": "Invalid request. The transaction ID provided is not an original transaction ID." 4 | } -------------------------------------------------------------------------------- /tests/resources/models/extendSubscriptionRenewalDateResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "originalTransactionId": "2312412", 3 | "webOrderLineItemId": "9993", 4 | "success": true, 5 | "effectiveDate": 1698148900000 6 | } -------------------------------------------------------------------------------- /tests/resources/models/getImageListResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "imageIdentifiers": [ 3 | { 4 | "imageIdentifier": "a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890", 5 | "imageState": "APPROVED" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tests/resources/models/getMessageListResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "messageIdentifiers": [ 3 | { 4 | "messageIdentifier": "a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890", 5 | "messageState": "APPROVED" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tests/resources/models/getRefundHistoryResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "signedTransactions": [ 3 | "signed_transaction_one", 4 | "signed_transaction_two" 5 | ], 6 | "revision": "revision_output", 7 | "hasMore": true 8 | } -------------------------------------------------------------------------------- /tests/resources/models/getStatusOfSubscriptionRenewalDateExtensionsResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestIdentifier": "20fba8a0-2b80-4a7d-a17f-85c1854727f8", 3 | "complete": true, 4 | "completeDate": 1698148900000, 5 | "succeededCount": 30, 6 | "failedCount": 2 7 | } -------------------------------------------------------------------------------- /tests/resources/certs/testSigningKey.p8: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgSpP55ELdXswj9JRZ 3 | APRwtTfS4CNRqpKIs+28rNHiPAqhRANCAASs8nLES7b+goKslppNVOurf0MonZdw 4 | 3pb6TxS8Z/5j+UNY1sWK1ChxpuwNS9I3R50cfdQo/lA9PPhw6XIg8ytd 5 | -----END PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | time: "02:00" 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | time: "02:00" 13 | -------------------------------------------------------------------------------- /tests/resources/models/transactionHistoryResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "revision": "revision_output", 3 | "hasMore": true, 4 | "bundleId": "com.example", 5 | "appAppleId": 323232, 6 | "environment": "LocalTesting", 7 | "signedTransactions": [ 8 | "signed_transaction_value", 9 | "signed_transaction_value2" 10 | ] 11 | } -------------------------------------------------------------------------------- /tests/resources/models/decodedRealtimeRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "originalTransactionId": "99371282", 3 | "appAppleId": 531412, 4 | "productId": "com.example.product", 5 | "userLocale": "en-US", 6 | "requestIdentifier": "3db5c98d-8acf-4e29-831e-8e1f82f9f6e9", 7 | "environment": "LocalTesting", 8 | "signedDate": 1698148900000 9 | } 10 | -------------------------------------------------------------------------------- /tests/resources/models/getTestNotificationStatusResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "signedPayload": "signed_payload", 3 | "sendAttempts": [ 4 | { 5 | "attemptDate": 1698148900000, 6 | "sendAttemptResult": "NO_RESPONSE" 7 | }, { 8 | "attemptDate": 1698148950000, 9 | "sendAttemptResult": "SUCCESS" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /tests/resources/models/transactionHistoryResponseWithMalformedAppAppleId.json: -------------------------------------------------------------------------------- 1 | { 2 | "revision": "revision_output", 3 | "hasMore": 1, 4 | "bundleId": "com.example", 5 | "appAppleId": "hi", 6 | "environment": "LocalTesting", 7 | "signedTransactions": [ 8 | "signed_transaction_value", 9 | "signed_transaction_value2" 10 | ] 11 | } -------------------------------------------------------------------------------- /tests/resources/models/transactionHistoryResponseWithMalformedEnvironment.json: -------------------------------------------------------------------------------- 1 | { 2 | "revision": "revision_output", 3 | "hasMore": true, 4 | "bundleId": "com.example", 5 | "appAppleId": 323232, 6 | "environment": "LocalTestingxxx", 7 | "signedTransactions": [ 8 | "signed_transaction_value", 9 | "signed_transaction_value2" 10 | ] 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | .pytest_cache/ 6 | 7 | # pyenv 8 | .python-version 9 | 10 | # Environments 11 | .env 12 | .venv 13 | venv 14 | 15 | 16 | # mypy 17 | .mypy_cache/ 18 | .dmypy.json 19 | dmypy.json 20 | 21 | # JetBrains 22 | .idea/ 23 | 24 | /coverage.xml 25 | /.coverage 26 | 27 | .DS_Store 28 | 29 | _build 30 | _staging 31 | 32 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/AutoRenewStatus.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class AutoRenewStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The renewal status for an auto-renewable subscription. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/autorenewstatus 12 | """ 13 | OFF = 0 14 | ON = 1 15 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/ImageState.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import Enum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class ImageState(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The approval state of an image. 10 | 11 | https://developer.apple.com/documentation/retentionmessaging/imagestate 12 | """ 13 | PENDING = "PENDING" 14 | APPROVED = "APPROVED" 15 | REJECTED = "REJECTED" 16 | -------------------------------------------------------------------------------- /tests/resources/models/signedExternalPurchaseTokenNotification.json: -------------------------------------------------------------------------------- 1 | { 2 | "notificationType": "EXTERNAL_PURCHASE_TOKEN", 3 | "subtype": "UNREPORTED", 4 | "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", 5 | "version": "2.0", 6 | "signedDate": 1698148900000, 7 | "externalPurchaseToken": { 8 | "externalPurchaseId": "b2158121-7af9-49d4-9561-1f588205523e", 9 | "tokenCreationDate": 1698148950000, 10 | "appAppleId": 55555, 11 | "bundleId": "com.example" 12 | } 13 | } -------------------------------------------------------------------------------- /appstoreserverlibrary/models/Platform.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class Platform(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The platform on which the customer consumed the in-app purchase. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/platform 12 | """ 13 | UNDECLARED = 0 14 | APPLE = 1 15 | NON_APPLE = 2 16 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/MessageState.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import Enum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class MessageState(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The approval state of the message. 10 | 11 | https://developer.apple.com/documentation/retentionmessaging/messagestate 12 | """ 13 | PENDING = "PENDING" 14 | APPROVED = "APPROVED" 15 | REJECTED = "REJECTED" 16 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/OfferType.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class OfferType(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The type of offer. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/offertype 12 | """ 13 | INTRODUCTORY_OFFER = 1 14 | PROMOTIONAL_OFFER = 2 15 | OFFER_CODE = 3 16 | WIN_BACK_OFFER = 4 17 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/RevocationReason.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class RevocationReason(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The reason for a refunded transaction. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/revocationreason 12 | """ 13 | REFUNDED_DUE_TO_ISSUE = 1 14 | REFUNDED_FOR_OTHER_REASON = 0 15 | -------------------------------------------------------------------------------- /tests/resources/models/signedExternalPurchaseTokenSandboxNotification.json: -------------------------------------------------------------------------------- 1 | { 2 | "notificationType": "EXTERNAL_PURCHASE_TOKEN", 3 | "subtype": "UNREPORTED", 4 | "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", 5 | "version": "2.0", 6 | "signedDate": 1698148900000, 7 | "externalPurchaseToken": { 8 | "externalPurchaseId": "SANDBOX_b2158121-7af9-49d4-9561-1f588205523e", 9 | "tokenCreationDate": 1698148950000, 10 | "appAppleId": 55555, 11 | "bundleId": "com.example" 12 | } 13 | } -------------------------------------------------------------------------------- /appstoreserverlibrary/models/OrderLookupStatus.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class OrderLookupStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | A value that indicates whether the order ID in the request is valid for your app. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/orderlookupstatus 12 | """ 13 | VALID = 0 14 | INVALID = 1 15 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/PurchasePlatform.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import Enum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class PurchasePlatform(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | Values that represent Apple platforms. 10 | 11 | https://developer.apple.com/documentation/storekit/appstore/platform 12 | """ 13 | IOS = "iOS" 14 | MAC_OS = "macOS" 15 | TV_OS = "tvOS" 16 | VISION_OS = "visionOS" 17 | -------------------------------------------------------------------------------- /tests/resources/models/signedNotification.json: -------------------------------------------------------------------------------- 1 | { 2 | "notificationType": "SUBSCRIBED", 3 | "subtype": "INITIAL_BUY", 4 | "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", 5 | "data": { 6 | "environment": "LocalTesting", 7 | "appAppleId": 41234, 8 | "bundleId": "com.example", 9 | "bundleVersion": "1.2.3", 10 | "signedTransactionInfo": "signed_transaction_info_value", 11 | "signedRenewalInfo": "signed_renewal_info_value", 12 | "status": 1 13 | }, 14 | "version": "2.0", 15 | "signedDate": 1698148900000 16 | } -------------------------------------------------------------------------------- /appstoreserverlibrary/models/Status.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class Status(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The status of an auto-renewable subscription. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/status 12 | """ 13 | ACTIVE = 1 14 | EXPIRED = 2 15 | BILLING_RETRY = 3 16 | BILLING_GRACE_PERIOD = 4 17 | REVOKED = 5 18 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/UserStatus.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class UserStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The status of a customer's account within your app. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/userstatus 12 | """ 13 | UNDECLARED = 0 14 | ACTIVE = 1 15 | SUSPENDED = 2 16 | TERMINATED = 3 17 | LIMITED_ACCESS = 4 18 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/InAppOwnershipType.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import Enum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class InAppOwnershipType(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The relationship of the user with the family-shared purchase to which they have access. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype 12 | """ 13 | FAMILY_SHARED = "FAMILY_SHARED" 14 | PURCHASED = "PURCHASED" 15 | -------------------------------------------------------------------------------- /tests/resources/models/appTransaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "receiptType": "LocalTesting", 3 | "appAppleId": 531412, 4 | "bundleId": "com.example", 5 | "applicationVersion": "1.2.3", 6 | "versionExternalIdentifier": 512, 7 | "receiptCreationDate": 1698148900000, 8 | "originalPurchaseDate": 1698148800000, 9 | "originalApplicationVersion": "1.1.2", 10 | "deviceVerification": "device_verification_value", 11 | "deviceVerificationNonce": "48ccfa42-7431-4f22-9908-7e88983e105a", 12 | "preorderDate": 1698148700000, 13 | "appTransactionId": "71134", 14 | "originalPlatform": "iOS" 15 | } -------------------------------------------------------------------------------- /appstoreserverlibrary/models/RefundPreference.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class RefundPreference(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | A value that indicates your preferred outcome for the refund request. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/refundpreference 12 | """ 13 | UNDECLARED = 0 14 | PREFER_GRANT = 1 15 | PREFER_DECLINE = 2 16 | NO_PREFERENCE = 3 17 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/Environment.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import Enum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class Environment(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The server environment, either sandbox or production. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/environment 12 | """ 13 | SANDBOX = "Sandbox" 14 | PRODUCTION = "Production" 15 | XCODE = "Xcode" 16 | LOCAL_TESTING = "LocalTesting" # Used for unit testing 17 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/ExtendReasonCode.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class ExtendReasonCode(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The code that represents the reason for the subscription-renewal-date extension. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode 12 | """ 13 | UNDECLARED = 0 14 | CUSTOMER_SATISFACTION = 1 15 | OTHER = 2 16 | SERVICE_ISSUE_OR_OUTAGE = 3 17 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/ConsumptionStatus.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class ConsumptionStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | A value that indicates the extent to which the customer consumed the in-app purchase. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus 12 | """ 13 | UNDECLARED = 0 14 | NOT_CONSUMED = 1 15 | PARTIALLY_CONSUMED = 2 16 | FULLY_CONSUMED = 3 17 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/OfferDiscountType.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import Enum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class OfferDiscountType(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The payment mode for a discount offer on an In-App Purchase. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype 12 | """ 13 | FREE_TRIAL = "FREE_TRIAL" 14 | PAY_AS_YOU_GO = "PAY_AS_YOU_GO" 15 | PAY_UP_FRONT = "PAY_UP_FRONT" 16 | ONE_TIME = "ONE_TIME" -------------------------------------------------------------------------------- /appstoreserverlibrary/models/PriceIncreaseStatus.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class PriceIncreaseStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The status that indicates whether an auto-renewable subscription is subject to a price increase. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/priceincreasestatus 12 | """ 13 | CUSTOMER_HAS_NOT_RESPONDED = 0 14 | CUSTOMER_CONSENTED_OR_WAS_NOTIFIED_WITHOUT_NEEDING_CONSENT = 1 15 | -------------------------------------------------------------------------------- /tests/resources/models/signedConsumptionRequestNotification.json: -------------------------------------------------------------------------------- 1 | { 2 | "notificationType": "CONSUMPTION_REQUEST", 3 | "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", 4 | "data": { 5 | "environment": "LocalTesting", 6 | "appAppleId": 41234, 7 | "bundleId": "com.example", 8 | "bundleVersion": "1.2.3", 9 | "signedTransactionInfo": "signed_transaction_info_value", 10 | "signedRenewalInfo": "signed_renewal_info_value", 11 | "status": 1, 12 | "consumptionRequestReason": "UNINTENDED_PURCHASE" 13 | }, 14 | "version": "2.0", 15 | "signedDate": 1698148900000 16 | } -------------------------------------------------------------------------------- /appstoreserverlibrary/models/ExpirationIntent.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class ExpirationIntent(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The reason an auto-renewable subscription expired. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/expirationintent 12 | """ 13 | CUSTOMER_CANCELLED = 1 14 | BILLING_ERROR = 2 15 | CUSTOMER_DID_NOT_CONSENT_TO_PRICE_INCREASE = 3 16 | PRODUCT_NOT_AVAILABLE = 4 17 | OTHER = 5 18 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/TransactionReason.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import Enum 4 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 5 | 6 | class TransactionReason(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): 7 | """ 8 | The cause of a purchase transaction, which indicates whether it’s a customer’s purchase or a renewal for an auto-renewable subscription that the system initiates. 9 | 10 | https://developer.apple.com/documentation/appstoreserverapi/transactionreason 11 | """ 12 | PURCHASE = "PURCHASE" 13 | RENEWAL = "RENEWAL" 14 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/Type.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import Enum 4 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 5 | 6 | class Type(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): 7 | """ 8 | The type of in-app purchase products you can offer in your app. 9 | 10 | https://developer.apple.com/documentation/appstoreserverapi/type 11 | """ 12 | AUTO_RENEWABLE_SUBSCRIPTION = "Auto-Renewable Subscription" 13 | NON_CONSUMABLE = "Non-Consumable" 14 | CONSUMABLE = "Consumable" 15 | NON_RENEWING_SUBSCRIPTION ="Non-Renewing Subscription" 16 | -------------------------------------------------------------------------------- /tests/resources/models/signedSummaryNotification.json: -------------------------------------------------------------------------------- 1 | { 2 | "notificationType": "RENEWAL_EXTENSION", 3 | "subtype": "SUMMARY", 4 | "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", 5 | "version": "2.0", 6 | "signedDate": 1698148900000, 7 | "summary": { 8 | "environment": "LocalTesting", 9 | "appAppleId": 41234, 10 | "bundleId": "com.example", 11 | "productId": "com.example.product", 12 | "requestIdentifier": "efb27071-45a4-4aca-9854-2a1e9146f265", 13 | "storefrontCountryCodes": [ 14 | "CAN", 15 | "USA", 16 | "MEX" 17 | ], 18 | "succeededCount": 5, 19 | "failedCount": 2 20 | } 21 | } -------------------------------------------------------------------------------- /appstoreserverlibrary/models/UpdateAppAccountTokenRequest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | from attr import define 3 | import attr 4 | 5 | @define 6 | class UpdateAppAccountTokenRequest: 7 | """ 8 | The request body that contains an app account token value. 9 | 10 | https://developer.apple.com/documentation/appstoreserverapi/updateappaccounttokenrequest 11 | """ 12 | 13 | appAccountToken: str = attr.ib() 14 | """ 15 | The UUID that an app optionally generates to map a customer's in-app purchase with its resulting App Store transaction. 16 | 17 | https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken 18 | """ 19 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/Message.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | from uuid import UUID 4 | 5 | from attr import define 6 | import attr 7 | 8 | @define 9 | class Message: 10 | """ 11 | A message identifier you provide in a real-time response to your Get Retention Message endpoint. 12 | 13 | https://developer.apple.com/documentation/retentionmessaging/message 14 | """ 15 | 16 | messageIdentifier: Optional[UUID] = attr.ib(default=None) 17 | """ 18 | The identifier of the message to display to the customer. 19 | 20 | https://developer.apple.com/documentation/retentionmessaging/messageidentifier 21 | """ 22 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/ConsumptionRequestReason.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import Enum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class ConsumptionRequestReason(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The customer-provided reason for a refund request. 10 | 11 | https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason 12 | """ 13 | UNINTENDED_PURCHASE = "UNINTENDED_PURCHASE" 14 | FULFILLMENT_ISSUE = "FULFILLMENT_ISSUE" 15 | UNSATISFIED_WITH_PURCHASE = "UNSATISFIED_WITH_PURCHASE" 16 | LEGAL = "LEGAL" 17 | OTHER = "OTHER" 18 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/RealtimeRequestBody.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | from typing import Optional, Dict 4 | 5 | from attr import define 6 | import attr 7 | 8 | @define 9 | class RealtimeRequestBody: 10 | """ 11 | The request body the App Store server sends to your Get Retention Message endpoint. 12 | 13 | https://developer.apple.com/documentation/retentionmessaging/realtimerequestbody 14 | """ 15 | 16 | signedPayload: Optional[str] = attr.ib(default=None) 17 | """ 18 | The payload in JSON Web Signature (JWS) format, signed by the App Store. 19 | 20 | https://developer.apple.com/documentation/retentionmessaging/signedpayload 21 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/models/PlayTime.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class PlayTime(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | A value that indicates the amount of time that the customer used the app. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/playtime 12 | """ 13 | UNDECLARED = 0 14 | ZERO_TO_FIVE_MINUTES = 1 15 | FIVE_TO_SIXTY_MINUTES = 2 16 | ONE_TO_SIX_HOURS = 3 17 | SIX_HOURS_TO_TWENTY_FOUR_HOURS = 4 18 | ONE_DAY_TO_FOUR_DAYS = 5 19 | FOUR_DAYS_TO_SIXTEEN_DAYS = 6 20 | OVER_SIXTEEN_DAYS = 7 21 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/SendTestNotificationResponse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | 4 | from attr import define 5 | import attr 6 | 7 | @define 8 | class SendTestNotificationResponse: 9 | """ 10 | A response that contains the test notification token. 11 | 12 | https://developer.apple.com/documentation/appstoreserverapi/sendtestnotificationresponse 13 | """ 14 | 15 | testNotificationToken: Optional[str] = attr.ib(default=None) 16 | """ 17 | A unique identifier for a notification test that the App Store server sends to your server. 18 | 19 | https://developer.apple.com/documentation/appstoreserverapi/testnotificationtoken 20 | """ 21 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/AppTransactionInfoResponse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | 4 | from attr import define 5 | import attr 6 | 7 | @define 8 | class AppTransactionInfoResponse: 9 | """ 10 | A response that contains signed app transaction information for a customer. 11 | 12 | https://developer.apple.com/documentation/appstoreserverapi/apptransactioninforesponse 13 | """ 14 | 15 | signedAppTransactionInfo: Optional[str] = attr.ib(default=None) 16 | """ 17 | A customer’s app transaction information, signed by Apple, in JSON Web Signature (JWS) format. 18 | 19 | https://developer.apple.com/documentation/appstoreserverapi/jwsapptransaction 20 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/models/DefaultConfigurationRequest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | from typing import Optional 4 | from uuid import UUID 5 | 6 | from attr import define 7 | import attr 8 | 9 | @define 10 | class DefaultConfigurationRequest: 11 | """ 12 | The request body that contains the default configuration information. 13 | 14 | https://developer.apple.com/documentation/retentionmessaging/defaultconfigurationrequest 15 | """ 16 | 17 | messageIdentifier: Optional[UUID] = attr.ib(default=None) 18 | """ 19 | The message identifier of the message to configure as a default message. 20 | 21 | https://developer.apple.com/documentation/retentionmessaging/messageidentifier 22 | """ 23 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/TransactionInfoResponse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | 4 | from attr import define 5 | import attr 6 | 7 | @define 8 | class TransactionInfoResponse: 9 | """ 10 | A response that contains signed transaction information for a single transaction. 11 | 12 | https://developer.apple.com/documentation/appstoreserverapi/transactioninforesponse 13 | """ 14 | 15 | signedTransactionInfo: Optional[str] = attr.ib(default=None) 16 | """ 17 | A customer’s in-app purchase transaction, signed by Apple, in JSON Web Signature (JWS) format. 18 | 19 | https://developer.apple.com/documentation/appstoreserverapi/jwstransaction 20 | """ 21 | -------------------------------------------------------------------------------- /tests/resources/models/getNotificationHistoryResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "paginationToken": "57715481-805a-4283-8499-1c19b5d6b20a", 3 | "hasMore": true, 4 | "notificationHistory": [ 5 | { 6 | "sendAttempts": [ 7 | { 8 | "attemptDate": 1698148900000, 9 | "sendAttemptResult": "NO_RESPONSE" 10 | }, { 11 | "attemptDate": 1698148950000, 12 | "sendAttemptResult": "SUCCESS" 13 | } 14 | ], 15 | "signedPayload": "signed_payload_one" 16 | }, 17 | { 18 | "sendAttempts": [ 19 | { 20 | "attemptDate": 1698148800000, 21 | "sendAttemptResult": "CIRCULAR_REDIRECT" 22 | } 23 | ], 24 | "signedPayload": "signed_payload_two" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /appstoreserverlibrary/models/ResponseBodyV2.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | 4 | from attr import define 5 | import attr 6 | 7 | @define 8 | class ResponseBodyV2: 9 | """ 10 | The response body the App Store sends in a version 2 server notification. 11 | 12 | https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2 13 | """ 14 | 15 | signedPayload: Optional[str] = attr.ib(default=None) 16 | """ 17 | A cryptographically signed payload, in JSON Web Signature (JWS) format, containing the response body for a version 2 notification. 18 | 19 | https://developer.apple.com/documentation/appstoreservernotifications/signedpayload 20 | """ 21 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/AccountTenure.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class AccountTenure(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The age of the customer's account. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/accounttenure 12 | """ 13 | UNDECLARED = 0 14 | ZERO_TO_THREE_DAYS = 1 15 | THREE_DAYS_TO_TEN_DAYS = 2 16 | TEN_DAYS_TO_THIRTY_DAYS = 3 17 | THIRTY_DAYS_TO_NINETY_DAYS = 4 18 | NINETY_DAYS_TO_ONE_HUNDRED_EIGHTY_DAYS = 5 19 | ONE_HUNDRED_EIGHTY_DAYS_TO_THREE_HUNDRED_SIXTY_FIVE_DAYS = 6 20 | GREATER_THAN_THREE_HUNDRED_SIXTY_FIVE_DAYS = 7 21 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/DeliveryStatus.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class DeliveryStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | A value that indicates whether the app successfully delivered an in-app purchase that works properly. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/deliverystatus 12 | """ 13 | DELIVERED_AND_WORKING_PROPERLY = 0 14 | DID_NOT_DELIVER_DUE_TO_QUALITY_ISSUE = 1 15 | DELIVERED_WRONG_ITEM = 2 16 | DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE = 3 17 | DID_NOT_DELIVER_DUE_TO_IN_GAME_CURRENCY_CHANGE = 4 18 | DID_NOT_DELIVER_FOR_OTHER_REASON = 5 19 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/GetImageListResponse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | from typing import Optional, List 4 | 5 | from attr import define 6 | import attr 7 | 8 | from .GetImageListResponseItem import GetImageListResponseItem 9 | 10 | @define 11 | class GetImageListResponse: 12 | """ 13 | A response that contains status information for all images. 14 | 15 | https://developer.apple.com/documentation/retentionmessaging/getimagelistresponse 16 | """ 17 | 18 | imageIdentifiers: Optional[List[GetImageListResponseItem]] = attr.ib(default=None) 19 | """ 20 | An array of all image identifiers and their image state. 21 | 22 | https://developer.apple.com/documentation/retentionmessaging/getimagelistresponseitem 23 | """ 24 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/MassExtendRenewalDateResponse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | 4 | from attr import define 5 | import attr 6 | 7 | @define 8 | class MassExtendRenewalDateResponse: 9 | """ 10 | A response that indicates the server successfully received the subscription-renewal-date extension request. 11 | 12 | https://developer.apple.com/documentation/appstoreserverapi/massextendrenewaldateresponse 13 | """ 14 | 15 | requestIdentifier: Optional[str] = attr.ib(default=None) 16 | """ 17 | A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. 18 | 19 | https://developer.apple.com/documentation/appstoreserverapi/requestidentifier 20 | """ -------------------------------------------------------------------------------- /tests/test_promotional_offer_signature_creator.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import unittest 4 | from appstoreserverlibrary.promotional_offer import PromotionalOfferSignatureCreator 5 | 6 | from tests.util import read_data_from_binary_file 7 | from uuid import UUID 8 | 9 | 10 | class PromotionalOfferSignatureCreatorTest(unittest.TestCase): 11 | def test_signature_creator(self): 12 | signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') 13 | signature_creator = PromotionalOfferSignatureCreator(signing_key, 'keyId', 'bundleId') 14 | signature = signature_creator.create_signature("productId", "offerId", "appAccountToken", UUID("20fba8a0-2b80-4a7d-a17f-85c1854727f8"), 1698148900000) 15 | self.assertIsNotNone(signature) -------------------------------------------------------------------------------- /appstoreserverlibrary/models/GetMessageListResponse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | from typing import Optional, List 4 | 5 | from attr import define 6 | import attr 7 | 8 | from .GetMessageListResponseItem import GetMessageListResponseItem 9 | 10 | @define 11 | class GetMessageListResponse: 12 | """ 13 | A response that contains status information for all messages. 14 | 15 | https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponse 16 | """ 17 | 18 | messageIdentifiers: Optional[List[GetMessageListResponseItem]] = attr.ib(default=None) 19 | """ 20 | An array of all message identifiers and their message state. 21 | 22 | https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponseitem 23 | """ 24 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/UploadMessageImage.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | from uuid import UUID 4 | 5 | from attr import define 6 | import attr 7 | 8 | @define 9 | class UploadMessageImage: 10 | """ 11 | The definition of an image with its alternative text. 12 | 13 | https://developer.apple.com/documentation/retentionmessaging/uploadmessageimage 14 | """ 15 | 16 | imageIdentifier: UUID = attr.ib() 17 | """ 18 | The unique identifier of an image. 19 | 20 | https://developer.apple.com/documentation/retentionmessaging/imageidentifier 21 | """ 22 | 23 | altText: str = attr.ib(validator=attr.validators.max_len(150)) 24 | """ 25 | The alternative text you provide for the corresponding image. 26 | 27 | https://developer.apple.com/documentation/retentionmessaging/alttext 28 | """ 29 | -------------------------------------------------------------------------------- /tests/resources/models/signedRenewalInfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "expirationIntent": 1, 3 | "originalTransactionId": "12345", 4 | "autoRenewProductId": "com.example.product.2", 5 | "productId": "com.example.product", 6 | "autoRenewStatus": 1, 7 | "isInBillingRetryPeriod": true, 8 | "priceIncreaseStatus": 0, 9 | "gracePeriodExpiresDate": 1698148900000, 10 | "offerType": 2, 11 | "offerIdentifier": "abc.123", 12 | "signedDate": 1698148800000, 13 | "environment": "LocalTesting", 14 | "recentSubscriptionStartDate": 1698148800000, 15 | "renewalDate": 1698148850000, 16 | "renewalPrice": 9990, 17 | "currency": "USD", 18 | "offerDiscountType": "PAY_AS_YOU_GO", 19 | "eligibleWinBackOfferIds": [ 20 | "eligible1", 21 | "eligible2" 22 | ], 23 | "appTransactionId": "71134", 24 | "offerPeriod": "P1Y", 25 | "appAccountToken": "7e3fb20b-4cdb-47cc-936d-99d65f608138" 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/ci-prb.yml: -------------------------------------------------------------------------------- 1 | name: PR Builder 2 | permissions: 3 | contents: read 4 | on: 5 | pull_request: 6 | branches: [ main ] 7 | push: 8 | branches: [ main ] 9 | jobs: 10 | build: 11 | name: Python ${{ matrix.python }} ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | python: [ "3.8", "3.9", "3.10", "3.11", "3.12" ] 16 | os: [ ubuntu-latest ] 17 | steps: 18 | - name: Checkout Code 19 | uses: actions/checkout@v5 20 | - name: Set up Python ${{ matrix.python }} 21 | uses: actions/setup-python@v6 22 | with: 23 | python-version: ${{ matrix.python }} 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | - name: Run tests 29 | run: | 30 | python -m unittest 31 | -------------------------------------------------------------------------------- /.github/workflows/ci-release.yml: -------------------------------------------------------------------------------- 1 | name: Release Builder 2 | permissions: 3 | contents: read 4 | on: 5 | release: 6 | types: [published] 7 | jobs: 8 | build: 9 | # Only non-pre-release builds trigger a release 10 | if: "!github.event.release.prerelease" 11 | name: Python Release Builder 12 | runs-on: ubuntu-latest 13 | environment: pypi 14 | permissions: 15 | id-token: write 16 | steps: 17 | - uses: actions/checkout@v5 18 | - name: Set up Python 19 | uses: actions/setup-python@v6 20 | with: 21 | python-version: "3.11" 22 | - name: Install build 23 | run: >- 24 | python3 -m 25 | pip install 26 | build 27 | --user 28 | - name: Build the sdist and wheel 29 | run: >- 30 | python3 -m 31 | build 32 | - name: Publish to PyPI 33 | uses: pypa/gh-action-pypi-publish@release/v1 34 | -------------------------------------------------------------------------------- /.github/workflows/ci-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Snapshot Builder 2 | permissions: 3 | contents: read 4 | on: 5 | release: 6 | types: [published] 7 | jobs: 8 | build: 9 | # Pre-release builds trigger a snapshot being created 10 | if: "github.event.release.prerelease" 11 | name: Python Snapshot Builder 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v5 15 | - name: Set up Python 16 | uses: actions/setup-python@v6 17 | with: 18 | python-version: "3.11" 19 | - name: Install build 20 | run: >- 21 | python3 -m 22 | pip install 23 | build 24 | --user 25 | - name: Build the sdist and wheel 26 | run: >- 27 | python3 -m 28 | build 29 | - name: Publish to Test PyPI 30 | uses: pypa/gh-action-pypi-publish@release/v1 31 | with: 32 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 33 | repository-url: https://test.pypi.org/legacy/ -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import pathlib 4 | 5 | from setuptools import find_packages, setup 6 | 7 | here = pathlib.Path(__file__).parent.resolve() 8 | long_description = (here / "README.md").read_text(encoding="utf-8") 9 | 10 | setup( 11 | name="app-store-server-library", 12 | version="1.9.0", 13 | description="The App Store Server Library", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | packages=find_packages(exclude=["tests"]), 17 | python_requires=">=3.7, <4", 18 | install_requires=["attrs >= 21.3.0", 'PyJWT >= 2.6.0, < 3', 'requests >= 2.28.0, < 3', 'cryptography >= 40.0.0', 'pyOpenSSL >= 23.1.1', 'asn1==2.8.0', 'cattrs >= 23.1.2'], 19 | extras_require={ 20 | "async": ["httpx"], 21 | }, 22 | package_data={"appstoreserverlibrary": ["py.typed"]}, 23 | license="MIT", 24 | classifiers=["License :: OSI Approved :: MIT License"], 25 | ) 26 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/SendAttemptResult.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import Enum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class SendAttemptResult(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The success or error information the App Store server records when it attempts to send an App Store server notification to your server. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/sendattemptresult 12 | """ 13 | SUCCESS = "SUCCESS" 14 | TIMED_OUT = "TIMED_OUT" 15 | TLS_ISSUE = "TLS_ISSUE" 16 | CIRCULAR_REDIRECT = "CIRCULAR_REDIRECT" 17 | NO_RESPONSE = "NO_RESPONSE" 18 | SOCKET_ISSUE = "SOCKET_ISSUE" 19 | UNSUPPORTED_CHARSET = "UNSUPPORTED_CHARSET" 20 | INVALID_RESPONSE = "INVALID_RESPONSE" 21 | PREMATURE_CLOSE = "PREMATURE_CLOSE" 22 | UNSUCCESSFUL_HTTP_RESPONSE_CODE = "UNSUCCESSFUL_HTTP_RESPONSE_CODE" 23 | OTHER = "OTHER" 24 | -------------------------------------------------------------------------------- /tests/resources/models/signedTransaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "transactionId":"23456", 3 | "originalTransactionId":"12345", 4 | "webOrderLineItemId":"34343", 5 | "bundleId":"com.example", 6 | "productId":"com.example.product", 7 | "subscriptionGroupIdentifier":"55555", 8 | "purchaseDate":1698148900000, 9 | "originalPurchaseDate":1698148800000, 10 | "expiresDate":1698149000000, 11 | "quantity":1, 12 | "type":"Auto-Renewable Subscription", 13 | "appAccountToken": "7e3fb20b-4cdb-47cc-936d-99d65f608138", 14 | "inAppOwnershipType":"PURCHASED", 15 | "signedDate":1698148900000, 16 | "revocationReason": 1, 17 | "revocationDate": 1698148950000, 18 | "isUpgraded": true, 19 | "offerType":1, 20 | "offerIdentifier": "abc.123", 21 | "environment":"LocalTesting", 22 | "transactionReason":"PURCHASE", 23 | "storefront":"USA", 24 | "storefrontId":"143441", 25 | "price": 10990, 26 | "currency": "USD", 27 | "offerDiscountType": "PAY_AS_YOU_GO", 28 | "appTransactionId": "71134", 29 | "offerPeriod": "P1Y" 30 | } -------------------------------------------------------------------------------- /appstoreserverlibrary/models/FirstSendAttemptResult.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import Enum, unique 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | @unique 8 | class FirstSendAttemptResult(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): 9 | """ 10 | An error or result that the App Store server receives when attempting to send an App Store server notification to your server. 11 | 12 | https://developer.apple.com/documentation/appstoreserverapi/firstsendattemptresult 13 | """ 14 | SUCCESS = "SUCCESS" 15 | TIMED_OUT = "TIMED_OUT" 16 | TLS_ISSUE = "TLS_ISSUE" 17 | CIRCULAR_REDIRECT = "CIRCULAR_REDIRECT" 18 | NO_RESPONSE = "NO_RESPONSE" 19 | SOCKET_ISSUE = "SOCKET_ISSUE" 20 | UNSUPPORTED_CHARSET = "UNSUPPORTED_CHARSET" 21 | INVALID_RESPONSE = "INVALID_RESPONSE" 22 | PREMATURE_CLOSE = "PREMATURE_CLOSE" 23 | UNSUCCESSFUL_HTTP_RESPONSE_CODE = "UNSUCCESSFUL_HTTP_RESPONSE_CODE" 24 | OTHER = "OTHER" 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2023 Apple Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /appstoreserverlibrary/models/AlternateProduct.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | from uuid import UUID 4 | 5 | from attr import define 6 | import attr 7 | 8 | @define 9 | class AlternateProduct: 10 | """ 11 | A switch-plan message and product ID you provide in a real-time response to your Get Retention Message endpoint. 12 | 13 | https://developer.apple.com/documentation/retentionmessaging/alternateproduct 14 | """ 15 | 16 | messageIdentifier: Optional[UUID] = attr.ib(default=None) 17 | """ 18 | The message identifier of the text to display in the switch-plan retention message. 19 | 20 | https://developer.apple.com/documentation/retentionmessaging/messageidentifier 21 | """ 22 | 23 | productId: Optional[str] = attr.ib(default=None) 24 | """ 25 | The product identifier of the subscription the retention message suggests for your customer to switch to. 26 | 27 | https://developer.apple.com/documentation/retentionmessaging/productid 28 | """ 29 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/LifetimeDollarsPurchased.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class LifetimeDollarsPurchased(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased 12 | """ 13 | UNDECLARED = 0 14 | ZERO_DOLLARS = 1 15 | ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 2 16 | FIFTY_DOLLARS_TO_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 3 17 | ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 4 18 | FIVE_HUNDRED_DOLLARS_TO_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 5 19 | ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 6 20 | TWO_THOUSAND_DOLLARS_OR_GREATER = 7 21 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/LifetimeDollarsRefunded.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import IntEnum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class LifetimeDollarsRefunded(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | A value that indicates the dollar amount of refunds the customer has received in your app, since purchasing the app, across all platforms. 10 | 11 | https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded 12 | """ 13 | UNDECLARED = 0 14 | ZERO_DOLLARS = 1 15 | ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 2 16 | FIFTY_DOLLARS_TO_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 3 17 | ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 4 18 | FIVE_HUNDRED_DOLLARS_TO_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 5 19 | ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 6 20 | TWO_THOUSAND_DOLLARS_OR_GREATER = 7 21 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/Subtype.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import Enum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class Subtype(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | A string that provides details about select notification types in version 2. 10 | 11 | https://developer.apple.com/documentation/appstoreservernotifications/subtype 12 | """ 13 | INITIAL_BUY = "INITIAL_BUY" 14 | RESUBSCRIBE = "RESUBSCRIBE" 15 | DOWNGRADE = "DOWNGRADE" 16 | UPGRADE = "UPGRADE" 17 | AUTO_RENEW_ENABLED = "AUTO_RENEW_ENABLED" 18 | AUTO_RENEW_DISABLED = "AUTO_RENEW_DISABLED" 19 | VOLUNTARY = "VOLUNTARY" 20 | BILLING_RETRY = "BILLING_RETRY" 21 | PRICE_INCREASE = "PRICE_INCREASE" 22 | GRACE_PERIOD = "GRACE_PERIOD" 23 | PENDING = "PENDING" 24 | ACCEPTED = "ACCEPTED" 25 | BILLING_RECOVERY = "BILLING_RECOVERY" 26 | PRODUCT_NOT_FOR_SALE = "PRODUCT_NOT_FOR_SALE" 27 | SUMMARY = "SUMMARY" 28 | FAILURE = "FAILURE" 29 | UNREPORTED = "UNREPORTED" 30 | -------------------------------------------------------------------------------- /tests/resources/models/getAllSubscriptionStatusesResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment": "LocalTesting", 3 | "bundleId": "com.example", 4 | "appAppleId": 5454545, 5 | "data": [ 6 | { 7 | "subscriptionGroupIdentifier": "sub_group_one", 8 | "lastTransactions": [ 9 | { 10 | "status": 1, 11 | "originalTransactionId": "3749183", 12 | "signedTransactionInfo": "signed_transaction_one", 13 | "signedRenewalInfo": "signed_renewal_one" 14 | }, 15 | { 16 | "status": 5, 17 | "originalTransactionId": "5314314134", 18 | "signedTransactionInfo": "signed_transaction_two", 19 | "signedRenewalInfo": "signed_renewal_two" 20 | } 21 | ] 22 | }, 23 | { 24 | "subscriptionGroupIdentifier": "sub_group_two", 25 | "lastTransactions": [ 26 | { 27 | "status": 2, 28 | "originalTransactionId": "3413453", 29 | "signedTransactionInfo": "signed_transaction_three", 30 | "signedRenewalInfo": "signed_renewal_three" 31 | } 32 | ] 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /appstoreserverlibrary/models/GetImageListResponseItem.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | from typing import Optional 4 | from uuid import UUID 5 | 6 | from attr import define 7 | import attr 8 | 9 | from .ImageState import ImageState 10 | from .LibraryUtility import AttrsRawValueAware 11 | 12 | @define 13 | class GetImageListResponseItem(AttrsRawValueAware): 14 | """ 15 | An image identifier and state information for an image. 16 | 17 | https://developer.apple.com/documentation/retentionmessaging/getimagelistresponseitem 18 | """ 19 | 20 | imageIdentifier: Optional[UUID] = attr.ib(default=None) 21 | """ 22 | The identifier of the image. 23 | 24 | https://developer.apple.com/documentation/retentionmessaging/imageidentifier 25 | """ 26 | 27 | imageState: Optional[ImageState] = ImageState.create_main_attr('rawImageState') 28 | """ 29 | The current state of the image. 30 | 31 | https://developer.apple.com/documentation/retentionmessaging/imagestate 32 | """ 33 | 34 | rawImageState: Optional[str] = ImageState.create_raw_attr('imageState') 35 | """ 36 | See imageState 37 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/models/GetMessageListResponseItem.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | from typing import Optional 4 | from uuid import UUID 5 | 6 | from attr import define 7 | import attr 8 | 9 | from .MessageState import MessageState 10 | from .LibraryUtility import AttrsRawValueAware 11 | 12 | @define 13 | class GetMessageListResponseItem(AttrsRawValueAware): 14 | """ 15 | A message identifier and status information for a message. 16 | 17 | https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponseitem 18 | """ 19 | 20 | messageIdentifier: Optional[UUID] = attr.ib(default=None) 21 | """ 22 | The identifier of the message. 23 | 24 | https://developer.apple.com/documentation/retentionmessaging/messageidentifier 25 | """ 26 | 27 | messageState: Optional[MessageState] = MessageState.create_main_attr('rawMessageState') 28 | """ 29 | The current state of the message. 30 | 31 | https://developer.apple.com/documentation/retentionmessaging/messageState 32 | """ 33 | 34 | rawMessageState: Optional[str] = MessageState.create_raw_attr('messageState') 35 | """ 36 | See messageState 37 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/models/CheckTestNotificationResponse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import List, Optional 3 | 4 | from attr import define 5 | import attr 6 | 7 | from .SendAttemptItem import SendAttemptItem 8 | 9 | @define 10 | class CheckTestNotificationResponse: 11 | """ 12 | A response that contains the contents of the test notification sent by the App Store server and the result from your server. 13 | 14 | https://developer.apple.com/documentation/appstoreserverapi/checktestnotificationresponse 15 | """ 16 | 17 | signedPayload: Optional[str] = attr.ib(default=None) 18 | """ 19 | A cryptographically signed payload, in JSON Web Signature (JWS) format, containing the response body for a version 2 notification. 20 | 21 | https://developer.apple.com/documentation/appstoreservernotifications/signedpayload 22 | """ 23 | 24 | sendAttempts: Optional[List[SendAttemptItem]] = attr.ib(default=None) 25 | """ 26 | An array of information the App Store server records for its attempts to send the TEST notification to your server. The array may contain a maximum of six sendAttemptItem objects. 27 | 28 | https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem 29 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/models/NotificationHistoryResponseItem.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional, List 3 | 4 | from attr import define 5 | import attr 6 | 7 | from .SendAttemptItem import SendAttemptItem 8 | 9 | @define 10 | class NotificationHistoryResponseItem: 11 | """ 12 | The App Store server notification history record, including the signed notification payload and the result of the server's first send attempt. 13 | 14 | https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponseitem 15 | """ 16 | 17 | signedPayload: Optional[str] = attr.ib(default=None) 18 | """ 19 | A cryptographically signed payload, in JSON Web Signature (JWS) format, containing the response body for a version 2 notification. 20 | 21 | https://developer.apple.com/documentation/appstoreservernotifications/signedpayload 22 | """ 23 | 24 | sendAttempts: Optional[List[SendAttemptItem]] = attr.ib(default=None) 25 | """ 26 | An array of information the App Store server records for its attempts to send a notification to your server. The maximum number of entries in the array is six. 27 | 28 | https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem 29 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/models/SubscriptionGroupIdentifierItem.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional, List 3 | 4 | from attr import define 5 | import attr 6 | 7 | from .LastTransactionsItem import LastTransactionsItem 8 | 9 | @define 10 | class SubscriptionGroupIdentifierItem: 11 | """ 12 | Information for auto-renewable subscriptions, including signed transaction information and signed renewal information, for one subscription group. 13 | 14 | https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifieritem 15 | """ 16 | 17 | subscriptionGroupIdentifier: Optional[str] = attr.ib(default=None) 18 | """ 19 | The identifier of the subscription group that the subscription belongs to. 20 | 21 | https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier 22 | """ 23 | 24 | lastTransactions: Optional[List[LastTransactionsItem]] = attr.ib(default=None) 25 | """ 26 | An array of the most recent App Store-signed transaction information and App Store-signed renewal information for all auto-renewable subscriptions in the subscription group. 27 | 28 | https://developer.apple.com/documentation/appstoreserverapi/lasttransactionsitem 29 | """ 30 | -------------------------------------------------------------------------------- /.github/workflows/ci-release-docs.yml: -------------------------------------------------------------------------------- 1 | name: Doc Builder 2 | permissions: 3 | contents: read 4 | on: 5 | release: 6 | types: [published] 7 | jobs: 8 | build: 9 | name: Python Doc Builder 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v5 13 | - name: Set up Python 14 | uses: actions/setup-python@v6 15 | with: 16 | python-version: "3.11" 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install -r requirements.txt 21 | pip install -r docs/requirements.txt 22 | - name: Sphinx Api Docs 23 | run: sphinx-apidoc -F -H "App Store Server Library" -A "Apple Inc." -V "0.2.1" -e -a -o _staging . tests setup.py 24 | - name: Spinx build 25 | run: sphinx-build -b html _staging _build 26 | - name: Upload docs 27 | uses: actions/upload-pages-artifact@v4 28 | with: 29 | path: _build 30 | deploy: 31 | permissions: 32 | pages: write 33 | id-token: write 34 | environment: 35 | name: github-pages 36 | url: ${{ steps.deployment.outputs.page_url }} 37 | needs: build 38 | runs-on: ubuntu-latest 39 | name: Deploy docs 40 | steps: 41 | - name: Deploy 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/OrderLookupResponse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from attr import define 4 | from typing import List, Optional 5 | import attr 6 | 7 | from .LibraryUtility import AttrsRawValueAware 8 | from .OrderLookupStatus import OrderLookupStatus 9 | 10 | @define 11 | class OrderLookupResponse(AttrsRawValueAware): 12 | """ 13 | A response that includes the order lookup status and an array of signed transactions for the in-app purchases in the order. 14 | 15 | https://developer.apple.com/documentation/appstoreserverapi/orderlookupresponse 16 | """ 17 | 18 | status: Optional[OrderLookupStatus] = OrderLookupStatus.create_main_attr('rawStatus') 19 | """ 20 | The status that indicates whether the order ID is valid. 21 | 22 | https://developer.apple.com/documentation/appstoreserverapi/orderlookupstatus 23 | """ 24 | 25 | rawStatus: Optional[int] = OrderLookupStatus.create_raw_attr('status') 26 | """ 27 | See status 28 | """ 29 | 30 | signedTransactions: Optional[List[str]] = attr.ib(default=None) 31 | """ 32 | An array of in-app purchase transactions that are part of order, signed by Apple, in JSON Web Signature format. 33 | 34 | https://developer.apple.com/documentation/appstoreserverapi/jwstransaction 35 | """ 36 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/NotificationTypeV2.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import Enum 4 | 5 | from .LibraryUtility import AppStoreServerLibraryEnumMeta 6 | 7 | class NotificationTypeV2(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): 8 | """ 9 | The type that describes the in-app purchase or external purchase event for which the App Store sends the version 2 notification. 10 | 11 | https://developer.apple.com/documentation/appstoreservernotifications/notificationtype 12 | """ 13 | SUBSCRIBED = "SUBSCRIBED" 14 | DID_CHANGE_RENEWAL_PREF = "DID_CHANGE_RENEWAL_PREF" 15 | DID_CHANGE_RENEWAL_STATUS = "DID_CHANGE_RENEWAL_STATUS" 16 | OFFER_REDEEMED = "OFFER_REDEEMED" 17 | DID_RENEW = "DID_RENEW" 18 | EXPIRED = "EXPIRED" 19 | DID_FAIL_TO_RENEW = "DID_FAIL_TO_RENEW" 20 | GRACE_PERIOD_EXPIRED = "GRACE_PERIOD_EXPIRED" 21 | PRICE_INCREASE = "PRICE_INCREASE" 22 | REFUND = "REFUND" 23 | REFUND_DECLINED = "REFUND_DECLINED" 24 | CONSUMPTION_REQUEST = "CONSUMPTION_REQUEST" 25 | RENEWAL_EXTENDED = "RENEWAL_EXTENDED" 26 | REVOKE = "REVOKE" 27 | TEST = "TEST" 28 | RENEWAL_EXTENSION = "RENEWAL_EXTENSION" 29 | REFUND_REVERSED = "REFUND_REVERSED" 30 | EXTERNAL_PURCHASE_TOKEN = "EXTERNAL_PURCHASE_TOKEN" 31 | ONE_TIME_CHARGE = "ONE_TIME_CHARGE" -------------------------------------------------------------------------------- /appstoreserverlibrary/models/ExtendRenewalDateRequest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | 4 | from attr import define 5 | import attr 6 | 7 | from .ExtendReasonCode import ExtendReasonCode 8 | 9 | @define 10 | class ExtendRenewalDateRequest: 11 | """ 12 | The request body that contains subscription-renewal-extension data for an individual subscription. 13 | 14 | https://developer.apple.com/documentation/appstoreserverapi/extendrenewaldaterequest 15 | """ 16 | 17 | extendByDays: Optional[int] = attr.ib(default=None) 18 | """ 19 | The number of days to extend the subscription renewal date. 20 | 21 | https://developer.apple.com/documentation/appstoreserverapi/extendbydays 22 | maximum: 90 23 | """ 24 | 25 | extendReasonCode: Optional[ExtendReasonCode] = attr.ib(default=None) 26 | """ 27 | The reason code for the subscription date extension 28 | 29 | https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode 30 | """ 31 | 32 | requestIdentifier: Optional[str] = attr.ib(default=None) 33 | """ 34 | A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. 35 | 36 | https://developer.apple.com/documentation/appstoreserverapi/requestidentifier 37 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/models/RealtimeResponseBody.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | from typing import Optional 4 | 5 | from attr import define 6 | import attr 7 | 8 | from .Message import Message 9 | from .AlternateProduct import AlternateProduct 10 | from .PromotionalOffer import PromotionalOffer 11 | 12 | @define 13 | class RealtimeResponseBody: 14 | """ 15 | A response you provide to choose, in real time, a retention message the system displays to the customer. 16 | 17 | https://developer.apple.com/documentation/retentionmessaging/realtimeresponsebody 18 | """ 19 | 20 | message: Optional[Message] = attr.ib(default=None) 21 | """ 22 | A retention message that's text-based and can include an optional image. 23 | 24 | https://developer.apple.com/documentation/retentionmessaging/message 25 | """ 26 | 27 | alternateProduct: Optional[AlternateProduct] = attr.ib(default=None) 28 | """ 29 | A retention message with a switch-plan option. 30 | 31 | https://developer.apple.com/documentation/retentionmessaging/alternateproduct 32 | """ 33 | 34 | promotionalOffer: Optional[PromotionalOffer] = attr.ib(default=None) 35 | """ 36 | A retention message that includes a promotional offer. 37 | 38 | https://developer.apple.com/documentation/retentionmessaging/promotionaloffer 39 | """ 40 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/RefundHistoryResponse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from attr import define 4 | from typing import List, Optional 5 | import attr 6 | 7 | @define 8 | class RefundHistoryResponse: 9 | """ 10 | A response that contains an array of signed JSON Web Signature (JWS) refunded transactions, and paging information. 11 | 12 | https://developer.apple.com/documentation/appstoreserverapi/refundhistoryresponse 13 | """ 14 | 15 | signedTransactions: Optional[List[str]] = attr.ib(default=None) 16 | """ 17 | A list of up to 20 JWS transactions, or an empty array if the customer hasn't received any refunds in your app. The transactions are sorted in ascending order by revocationDate. 18 | 19 | https://developer.apple.com/documentation/appstoreserverapi/jwstransaction 20 | """ 21 | 22 | revision: Optional[str] = attr.ib(default=None) 23 | """ 24 | A token you use in a query to request the next set of transactions for the customer. 25 | 26 | https://developer.apple.com/documentation/appstoreserverapi/revision 27 | """ 28 | 29 | hasMore: Optional[bool] = attr.ib(default=None) 30 | """ 31 | A Boolean value indicating whether the App Store has more transaction data. 32 | 33 | https://developer.apple.com/documentation/appstoreserverapi/hasmore 34 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/models/UploadMessageRequestBody.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | from typing import Optional 4 | 5 | from attr import define 6 | import attr 7 | 8 | from .UploadMessageImage import UploadMessageImage 9 | 10 | @define 11 | class UploadMessageRequestBody: 12 | """ 13 | The request body for uploading a message, which includes the message text and an optional image reference. 14 | 15 | https://developer.apple.com/documentation/retentionmessaging/uploadmessagerequestbody 16 | """ 17 | 18 | header: str = attr.ib(validator=attr.validators.max_len(66)) 19 | """ 20 | The header text of the retention message that the system displays to customers. 21 | 22 | https://developer.apple.com/documentation/retentionmessaging/header 23 | """ 24 | 25 | body: str = attr.ib(validator=attr.validators.max_len(144)) 26 | """ 27 | The body text of the retention message that the system displays to customers. 28 | 29 | https://developer.apple.com/documentation/retentionmessaging/body 30 | """ 31 | 32 | image: Optional[UploadMessageImage] = attr.ib(default=None) 33 | """ 34 | The optional image identifier and its alternative text to appear as part of a text-based message with an image. 35 | 36 | https://developer.apple.com/documentation/retentionmessaging/uploadmessageimage 37 | """ 38 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/PromotionalOffer.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | from typing import Optional 4 | from uuid import UUID 5 | 6 | from attr import define 7 | import attr 8 | 9 | from .PromotionalOfferSignatureV1 import PromotionalOfferSignatureV1 10 | 11 | @define 12 | class PromotionalOffer: 13 | """ 14 | A promotional offer and message you provide in a real-time response to your Get Retention Message endpoint. 15 | 16 | https://developer.apple.com/documentation/retentionmessaging/promotionaloffer 17 | """ 18 | 19 | messageIdentifier: Optional[UUID] = attr.ib(default=None) 20 | """ 21 | The identifier of the message to display to the customer, along with the promotional offer. 22 | 23 | https://developer.apple.com/documentation/retentionmessaging/messageidentifier 24 | """ 25 | 26 | promotionalOfferSignatureV2: Optional[str] = attr.ib(default=None) 27 | """ 28 | The promotional offer signature in V2 format. 29 | 30 | https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev2 31 | """ 32 | 33 | promotionalOfferSignatureV1: Optional[PromotionalOfferSignatureV1] = attr.ib(default=None) 34 | """ 35 | The promotional offer signature in V1 format. 36 | 37 | https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1 38 | """ 39 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/SendAttemptItem.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | 4 | from attr import define 5 | import attr 6 | 7 | from .LibraryUtility import AttrsRawValueAware 8 | from .SendAttemptResult import SendAttemptResult 9 | 10 | @define 11 | class SendAttemptItem(AttrsRawValueAware): 12 | """ 13 | The success or error information and the date the App Store server records when it attempts to send a server notification to your server. 14 | 15 | https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem 16 | """ 17 | 18 | attemptDate: Optional[int] = attr.ib(default=None) 19 | """ 20 | The date the App Store server attempts to send a notification. 21 | 22 | https://developer.apple.com/documentation/appstoreserverapi/attemptdate 23 | """ 24 | 25 | sendAttemptResult: Optional[SendAttemptResult] = SendAttemptResult.create_main_attr('rawSendAttemptResult') 26 | """ 27 | The success or error information the App Store server records when it attempts to send an App Store server notification to your server. 28 | 29 | https://developer.apple.com/documentation/appstoreserverapi/sendattemptresult 30 | """ 31 | 32 | rawSendAttemptResult: Optional[str] = SendAttemptResult.create_raw_attr('sendAttemptResult') 33 | """ 34 | See sendAttemptResult 35 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/models/NotificationHistoryResponse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional, List 3 | 4 | from attr import define 5 | import attr 6 | 7 | from .NotificationHistoryResponseItem import NotificationHistoryResponseItem 8 | 9 | @define 10 | class NotificationHistoryResponse: 11 | """ 12 | A response that contains the App Store Server Notifications history for your app. 13 | 14 | https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponse 15 | """ 16 | 17 | paginationToken: Optional[str] = attr.ib(default=None) 18 | """ 19 | A pagination token that you return to the endpoint on a subsequent call to receive the next set of results. 20 | 21 | https://developer.apple.com/documentation/appstoreserverapi/paginationtoken 22 | """ 23 | 24 | hasMore: Optional[bool] = attr.ib(default=None) 25 | """ 26 | A Boolean value indicating whether the App Store has more transaction data. 27 | 28 | https://developer.apple.com/documentation/appstoreserverapi/hasmore 29 | """ 30 | 31 | notificationHistory: Optional[List[NotificationHistoryResponseItem]] = attr.ib(default=None) 32 | """ 33 | An array of App Store server notification history records. 34 | 35 | https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponseitem 36 | """ 37 | -------------------------------------------------------------------------------- /tests/resources/xcode/xcode-signed-renewal-info: -------------------------------------------------------------------------------- 1 | eyJraWQiOiJBcHBsZV9YY29kZV9LZXkiLCJ0eXAiOiJKV1QiLCJ4NWMiOlsiTUlJQnpEQ0NBWEdnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpCSU1TSXdJQVlEVlFRREV4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1TSXdJQVlEVlFRS0V4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1CNFhEVEl6TVRBeE9UQXhORFV6TmxvWERUSTBNVEF4T0RBeE5EVXpObG93U0RFaU1DQUdBMVVFQXhNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRFaU1DQUdBMVVFQ2hNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQktYRVFnWWpDb3VQdFRzdEdyS3BZOEk1M25IN3JiREhuY0lMR25vZ1NBdWxJSTNzXC91Zk0wZzlEYzNCY3I0OTdBVWd6R1R2V3Bpd0p4cGVCMzcxTmdWK2pUREJLTUJJR0ExVWRFd0VCXC93UUlNQVlCQWY4Q0FRQXdKQVlEVlIwUkJCMHdHNEVaVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEFPQmdOVkhROEJBZjhFQkFNQ0I0QXdDZ1lJS29aSXpqMEVBd0lEU1FBd1JnSWhBTVp2VllKNjRDRitoMmZtc213dnpBY2VQcklEMTNycElKR0JFVytXZ3BwdEFpRUF4V2l5NCtUMXp0MzdWc3UwdmI2WXVtMCtOTHREcUhsSzZycE1jdjZKZm5BPSJdLCJhbGciOiJFUzI1NiJ9.eyJkZXZpY2VWZXJpZmljYXRpb24iOiJ1K1cxb1FUcXZGSE9RK1pCZTRRMHhQTUMyOGtxRUZ2YmJzRVBwTEtEVlJGdjFHSkdlZ21yTkhWb09ZTU9QdmIyIiwicHJvZHVjdElkIjoicGFzcy5wcmVtaXVtIiwiZGV2aWNlVmVyaWZpY2F0aW9uTm9uY2UiOiIzNDM5OTE5ZS04N2M5LTQ3YjYtYWVlZS0yODIzZjdhOWQzYzMiLCJyZW5ld2FsRGF0ZSI6MTcwMDM1ODMzNjA0OS43Mjk3LCJvcmlnaW5hbFRyYW5zYWN0aW9uSWQiOiIwIiwicmVjZW50U3Vic2NyaXB0aW9uU3RhcnREYXRlIjoxNjk3Njc5OTM2MDQ5LjcyOTcsImF1dG9SZW5ld1N0YXR1cyI6MSwic2lnbmVkRGF0ZSI6MTY5NzY3OTkzNjcxMS4wNzQ3LCJlbnZpcm9ubWVudCI6Ilhjb2RlIiwiYXV0b1JlbmV3UHJvZHVjdElkIjoicGFzcy5wcmVtaXVtIn0.WnT3aB9Lwjbr0ICUGn_5CdglzedVd7eOkrqirhcWFvwJZzN1FajuMV6gFEbgD82aL0Ix6HGZcwkNDlVNLvYOEQ -------------------------------------------------------------------------------- /tests/resources/xcode/xcode-signed-app-transaction: -------------------------------------------------------------------------------- 1 | eyJ4NWMiOlsiTUlJQnpEQ0NBWEdnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpCSU1TSXdJQVlEVlFRREV4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1TSXdJQVlEVlFRS0V4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1CNFhEVEl6TVRBeE9UQXhORFV6TmxvWERUSTBNVEF4T0RBeE5EVXpObG93U0RFaU1DQUdBMVVFQXhNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRFaU1DQUdBMVVFQ2hNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQktYRVFnWWpDb3VQdFRzdEdyS3BZOEk1M25IN3JiREhuY0lMR25vZ1NBdWxJSTNzXC91Zk0wZzlEYzNCY3I0OTdBVWd6R1R2V3Bpd0p4cGVCMzcxTmdWK2pUREJLTUJJR0ExVWRFd0VCXC93UUlNQVlCQWY4Q0FRQXdKQVlEVlIwUkJCMHdHNEVaVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEFPQmdOVkhROEJBZjhFQkFNQ0I0QXdDZ1lJS29aSXpqMEVBd0lEU1FBd1JnSWhBTVp2VllKNjRDRitoMmZtc213dnpBY2VQcklEMTNycElKR0JFVytXZ3BwdEFpRUF4V2l5NCtUMXp0MzdWc3UwdmI2WXVtMCtOTHREcUhsSzZycE1jdjZKZm5BPSJdLCJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkFwcGxlX1hjb2RlX0tleSJ9.eyJidW5kbGVJZCI6ImNvbS5leGFtcGxlLm5hdHVyZWxhYi5iYWNreWFyZGJpcmRzLmV4YW1wbGUiLCJhcHBsaWNhdGlvblZlcnNpb24iOiIxIiwiZGV2aWNlVmVyaWZpY2F0aW9uTm9uY2UiOiI0OGM4YjkyZC1jZTBkLTQyMjktYmVkZi1lNjFiNGY5Y2ZjOTIiLCJyZWNlaXB0VHlwZSI6Ilhjb2RlIiwicmVjZWlwdENyZWF0aW9uRGF0ZSI6MTY5NzY4MDEyMjI1Ny40NDcsImRldmljZVZlcmlmaWNhdGlvbiI6ImNZVXNYYzUzRWJZYzBwT2VYRzVkNlwvMzFMR0hlVkdmODRzcVNOME9ySmk1dVwvajJIODlXV0tnUzhOMGhNc01sZiIsInJlcXVlc3REYXRlIjoxNjk3NjgwMTIyMjU3LjQ0Nywib3JpZ2luYWxBcHBsaWNhdGlvblZlcnNpb24iOiIxIiwib3JpZ2luYWxQdXJjaGFzZURhdGUiOi02MjEzNTc2OTYwMDAwMH0.Dpdk_VsO2MUCevwyS407alJpPc1Nq_UIP9EiDHaQBxlyi35NFnsKUVNuFNcGWrGRCCImnb4QGBKHfQC2i4sPCg -------------------------------------------------------------------------------- /appstoreserverlibrary/models/ExtendRenewalDateResponse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | 4 | from attr import define 5 | import attr 6 | 7 | @define 8 | class ExtendRenewalDateResponse: 9 | """ 10 | A response that indicates whether an individual renewal-date extension succeeded, and related details. 11 | 12 | https://developer.apple.com/documentation/appstoreserverapi/extendrenewaldateresponse 13 | """ 14 | 15 | originalTransactionId: Optional[str] = attr.ib(default=None) 16 | """ 17 | The original transaction identifier of a purchase. 18 | 19 | https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid 20 | """ 21 | 22 | webOrderLineItemId: Optional[str] = attr.ib(default=None) 23 | """ 24 | The unique identifier of subscription-purchase events across devices, including renewals. 25 | 26 | https://developer.apple.com/documentation/appstoreserverapi/weborderlineitemid 27 | """ 28 | 29 | success: Optional[bool] = attr.ib(default=None) 30 | """ 31 | A Boolean value that indicates whether the subscription-renewal-date extension succeeded. 32 | 33 | https://developer.apple.com/documentation/appstoreserverapi/success 34 | """ 35 | 36 | effectiveDate: Optional[int] = attr.ib(default=None) 37 | """ 38 | The new subscription expiration date for a subscription-renewal extension. 39 | 40 | https://developer.apple.com/documentation/appstoreserverapi/effectivedate 41 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/models/ExternalPurchaseToken.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | 4 | from attr import define 5 | import attr 6 | 7 | from .LibraryUtility import AttrsRawValueAware 8 | 9 | @define 10 | class ExternalPurchaseToken(AttrsRawValueAware): 11 | """ 12 | The payload data that contains an external purchase token. 13 | 14 | https://developer.apple.com/documentation/appstoreservernotifications/externalpurchasetoken 15 | """ 16 | 17 | externalPurchaseId: Optional[str] = attr.ib(default=None) 18 | """ 19 | The field of an external purchase token that uniquely identifies the token. 20 | 21 | https://developer.apple.com/documentation/appstoreservernotifications/externalpurchaseid 22 | """ 23 | 24 | tokenCreationDate: Optional[int] = attr.ib(default=None) 25 | """ 26 | The field of an external purchase token that contains the UNIX date, in milliseconds, when the system created the token. 27 | 28 | https://developer.apple.com/documentation/appstoreservernotifications/tokencreationdate 29 | """ 30 | 31 | appAppleId: Optional[int] = attr.ib(default=None) 32 | """ 33 | The unique identifier of an app in the App Store. 34 | 35 | https://developer.apple.com/documentation/appstoreservernotifications/appappleid 36 | """ 37 | 38 | bundleId: Optional[str] = attr.ib(default=None) 39 | """ 40 | The bundle identifier of an app. 41 | 42 | https://developer.apple.com/documentation/appstoreservernotifications/bundleid 43 | """ -------------------------------------------------------------------------------- /tests/test_receipt_utility.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import unittest 4 | from appstoreserverlibrary.receipt_utility import ReceiptUtility 5 | 6 | from tests.util import read_data_from_file 7 | 8 | APP_RECEIPT_EXPECTED_TRANSACTION_ID = "0" 9 | TRANSACTION_RECEIPT_EXPECTED_TRANSACTION_ID = "33993399" 10 | 11 | class ReceiptUtilityTest(unittest.TestCase): 12 | def test_xcode_app_receipt_extraction_with_no_transactions(self): 13 | receipt = read_data_from_file("tests/resources/xcode/xcode-app-receipt-empty") 14 | 15 | receipt_util = ReceiptUtility() 16 | 17 | extracted_transaction_id = receipt_util.extract_transaction_id_from_app_receipt(receipt) 18 | 19 | self.assertIsNone(extracted_transaction_id) 20 | 21 | def test_xcode_app_receipt_extraction_with_transactions(self): 22 | receipt = read_data_from_file("tests/resources/xcode/xcode-app-receipt-with-transaction") 23 | 24 | receipt_util = ReceiptUtility() 25 | 26 | extracted_transaction_id = receipt_util.extract_transaction_id_from_app_receipt(receipt) 27 | 28 | self.assertEqual(APP_RECEIPT_EXPECTED_TRANSACTION_ID, extracted_transaction_id) 29 | 30 | def test_transaction_receipt_extraction(self): 31 | receipt = read_data_from_file("tests/resources/mock_signed_data/legacyTransaction") 32 | 33 | receipt_util = ReceiptUtility() 34 | 35 | extracted_transaction_id = receipt_util.extract_transaction_id_from_transaction_receipt(receipt) 36 | 37 | self.assertEqual(TRANSACTION_RECEIPT_EXPECTED_TRANSACTION_ID, extracted_transaction_id) -------------------------------------------------------------------------------- /appstoreserverlibrary/models/PromotionalOfferSignatureV1.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | from typing import Optional 4 | from uuid import UUID 5 | 6 | from attr import define 7 | import attr 8 | 9 | @define 10 | class PromotionalOfferSignatureV1: 11 | """ 12 | The promotional offer signature you generate using an earlier signature version. 13 | 14 | https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1 15 | """ 16 | 17 | encodedSignature: str = attr.ib() 18 | """ 19 | The Base64-encoded cryptographic signature you generate using the offer parameters. 20 | """ 21 | 22 | productId: str = attr.ib() 23 | """ 24 | The subscription's product identifier. 25 | 26 | https://developer.apple.com/documentation/retentionmessaging/productid 27 | """ 28 | 29 | nonce: UUID = attr.ib() 30 | """ 31 | A one-time-use UUID antireplay value you generate. 32 | """ 33 | 34 | timestamp: int = attr.ib() 35 | """ 36 | The UNIX time, in milliseconds, when you generate the signature. 37 | """ 38 | 39 | keyId: str = attr.ib() 40 | """ 41 | A string that identifies the private key you use to generate the signature. 42 | """ 43 | 44 | offerIdentifier: str = attr.ib() 45 | """ 46 | The subscription offer identifier that you set up in App Store Connect. 47 | """ 48 | 49 | appAccountToken: Optional[UUID] = attr.ib(default=None) 50 | """ 51 | A UUID that you provide to associate with the transaction if the customer accepts the promotional offer. 52 | """ 53 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/LastTransactionsItem.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | 4 | from attr import define 5 | import attr 6 | 7 | from .LibraryUtility import AttrsRawValueAware 8 | from .Status import Status 9 | 10 | @define 11 | class LastTransactionsItem(AttrsRawValueAware): 12 | """ 13 | The most recent App Store-signed transaction information and App Store-signed renewal information for an auto-renewable subscription. 14 | 15 | https://developer.apple.com/documentation/appstoreserverapi/lasttransactionsitem 16 | """ 17 | 18 | status: Optional[Status] = Status.create_main_attr('rawStatus') 19 | """ 20 | The status of the auto-renewable subscription. 21 | 22 | https://developer.apple.com/documentation/appstoreserverapi/status 23 | """ 24 | 25 | rawStatus: Optional[int] = Status.create_raw_attr('status') 26 | """ 27 | See status 28 | """ 29 | 30 | originalTransactionId: Optional[str] = attr.ib(default=None) 31 | """ 32 | The original transaction identifier of a purchase. 33 | 34 | https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid 35 | """ 36 | 37 | signedTransactionInfo: Optional[str] = attr.ib(default=None) 38 | """ 39 | Transaction information signed by the App Store, in JSON Web Signature (JWS) format. 40 | 41 | https://developer.apple.com/documentation/appstoreserverapi/jwstransaction 42 | """ 43 | 44 | signedRenewalInfo: Optional[str] = attr.ib(default=None) 45 | """ 46 | Subscription renewal information, signed by the App Store, in JSON Web Signature (JWS) format. 47 | 48 | https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo 49 | """ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing! 4 | 5 | ## Reporting Bugs 6 | 7 | Please report bugs by creating [Github issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/about-issues). 8 | To help the community understand the bug and get it fixed faster, please provide the following information when creating a new issue: 9 | - A clear and descriptive title 10 | - The exact steps to reproduce the bug 11 | - The observed behavior and expected behavior 12 | 13 | If possible, also include payloads, commands, screenshots, etc to help the community identify the problem. Do not include any personal or sensitive data. 14 | 15 | ## Suggesting Improvements 16 | 17 | You can suggest improvements also by creating Github issues. 18 | When creating a new suggestion, please provide the following information: 19 | - A clear and descriptive title 20 | - A description of the proposed improvement in as many details as possible 21 | - Explain why the improvement is important 22 | 23 | ## Documentation Contribution 24 | 25 | Documentation contribution will make it easier for the community to work on the project. 26 | You may add README/diagrams to the components, or improve the existing docs. For major doc changes, we encourage you to create issues before contributing. Let us know what you are planning to change before the contribution. 27 | 28 | ## Code Contribution 29 | 30 | For minor changes (like small bug fixes or typo correction), feel free to open up a PR directly. 31 | For new features or major changes, we encourage you to create a Github issue first, and get agreement before starting on the implementation. This is to save you time in case there's duplicate effort or unforeseen risk. 32 | 33 | ## Project Licensing 34 | 35 | All contributions (including Pull Requests) to this project are provided under the terms of the project’s [LICENSE](LICENSE.txt) -------------------------------------------------------------------------------- /tests/resources/xcode/xcode-signed-transaction: -------------------------------------------------------------------------------- 1 | eyJraWQiOiJBcHBsZV9YY29kZV9LZXkiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlCekRDQ0FYR2dBd0lCQWdJQkFUQUtCZ2dxaGtqT1BRUURBakJJTVNJd0lBWURWUVFERXhsVGRHOXlaVXRwZENCVVpYTjBhVzVuSUdsdUlGaGpiMlJsTVNJd0lBWURWUVFLRXhsVGRHOXlaVXRwZENCVVpYTjBhVzVuSUdsdUlGaGpiMlJsTUI0WERUSXpNVEF4T1RBeE5EVXpObG9YRFRJME1UQXhPREF4TkRVek5sb3dTREVpTUNBR0ExVUVBeE1aVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEVpTUNBR0ExVUVDaE1aVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCS1hFUWdZakNvdVB0VHN0R3JLcFk4STUzbkg3cmJESG5jSUxHbm9nU0F1bElJM3NcL3VmTTBnOURjM0JjcjQ5N0FVZ3pHVHZXcGl3SnhwZUIzNzFOZ1YralREQktNQklHQTFVZEV3RUJcL3dRSU1BWUJBZjhDQVFBd0pBWURWUjBSQkIwd0c0RVpVM1J2Y21WTGFYUWdWR1Z6ZEdsdVp5QnBiaUJZWTI5a1pUQU9CZ05WSFE4QkFmOEVCQU1DQjRBd0NnWUlLb1pJemowRUF3SURTUUF3UmdJaEFNWnZWWUo2NENGK2gyZm1zbXd2ekFjZVBySUQxM3JwSUpHQkVXK1dncHB0QWlFQXhXaXk0K1QxenQzN1ZzdTB2YjZZdW0wK05MdERxSGxLNnJwTWN2NkpmbkE9Il19.eyJpbkFwcE93bmVyc2hpcFR5cGUiOiJQVVJDSEFTRUQiLCJwdXJjaGFzZURhdGUiOjE2OTc2Nzk5MzYwNDkuNzI5Nywic3Vic2NyaXB0aW9uR3JvdXBJZGVudGlmaWVyIjoiNkYzQTkzQUIiLCJzaWduZWREYXRlIjoxNjk3Njc5OTM2MDU2LjQ4NSwib3JpZ2luYWxQdXJjaGFzZURhdGUiOjE2OTc2Nzk5MzYwNDkuNzI5NywiaXNVcGdyYWRlZCI6ZmFsc2UsImRldmljZVZlcmlmaWNhdGlvbiI6InNHRG5wZytvemI4dXdEU3VDRFoyb1ZabzFDS3JiQjh1alI4VnhDeGh5a1J3eUJJSzZ4NlhDeUVSbTh5V3J6RTgiLCJvZmZlclR5cGUiOjEsInF1YW50aXR5IjoxLCJ0cmFuc2FjdGlvbklkIjoiMCIsInR5cGUiOiJBdXRvLVJlbmV3YWJsZSBTdWJzY3JpcHRpb24iLCJ0cmFuc2FjdGlvblJlYXNvbiI6IlBVUkNIQVNFIiwicHJvZHVjdElkIjoicGFzcy5wcmVtaXVtIiwiZXhwaXJlc0RhdGUiOjE3MDAzNTgzMzYwNDkuNzI5NywiZW52aXJvbm1lbnQiOiJYY29kZSIsInN0b3JlZnJvbnRJZCI6IjE0MzQ0MSIsIm9yaWdpbmFsVHJhbnNhY3Rpb25JZCI6IjAiLCJidW5kbGVJZCI6ImNvbS5leGFtcGxlLm5hdHVyZWxhYi5iYWNreWFyZGJpcmRzLmV4YW1wbGUiLCJkZXZpY2VWZXJpZmljYXRpb25Ob25jZSI6IjdlZGVhODdkLTk4ZjAtNDJkMC05NjgyLTQ5Y2E4MTAyMmY3MyIsIndlYk9yZGVyTGluZUl0ZW1JZCI6IjAiLCJzdG9yZWZyb250IjoiVVNBIn0.rkJYnvujStteRkMHhoIR2ThmNFnyKcx5XxIakXYdh-1oKtEVEU5zQAiONaLDpBDO5JhLLrTbfp7LS5tMiqmgHw -------------------------------------------------------------------------------- /appstoreserverlibrary/models/StatusResponse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional, List 3 | 4 | from attr import define 5 | import attr 6 | 7 | from .Environment import Environment 8 | from .LibraryUtility import AttrsRawValueAware 9 | from .SubscriptionGroupIdentifierItem import SubscriptionGroupIdentifierItem 10 | 11 | @define 12 | class StatusResponse(AttrsRawValueAware): 13 | """ 14 | A response that contains status information for all of a customer's auto-renewable subscriptions in your app. 15 | 16 | https://developer.apple.com/documentation/appstoreserverapi/statusresponse 17 | """ 18 | 19 | environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment') 20 | """ 21 | The server environment, sandbox or production, in which the App Store generated the response. 22 | 23 | https://developer.apple.com/documentation/appstoreserverapi/environment 24 | """ 25 | 26 | rawEnvironment: Optional[str] = Environment.create_raw_attr('environment') 27 | """ 28 | See environment 29 | """ 30 | 31 | bundleId: Optional[str] = attr.ib(default=None) 32 | """ 33 | The bundle identifier of an app. 34 | 35 | https://developer.apple.com/documentation/appstoreserverapi/bundleid 36 | """ 37 | 38 | appAppleId: Optional[int] = attr.ib(default=None) 39 | """ 40 | The unique identifier of an app in the App Store. 41 | 42 | https://developer.apple.com/documentation/appstoreservernotifications/appappleid 43 | """ 44 | 45 | data: Optional[List[SubscriptionGroupIdentifierItem]] = attr.ib(default=None) 46 | """ 47 | An array of information for auto-renewable subscriptions, including App Store-signed transaction information and App Store-signed renewal information. 48 | 49 | https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifieritem 50 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/models/MassExtendRenewalDateRequest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from attr import define 4 | from typing import List, Optional 5 | import attr 6 | 7 | from .ExtendReasonCode import ExtendReasonCode 8 | 9 | @define 10 | class MassExtendRenewalDateRequest: 11 | """ 12 | The request body that contains subscription-renewal-extension data to apply for all eligible active subscribers. 13 | 14 | https://developer.apple.com/documentation/appstoreserverapi/massextendrenewaldaterequest 15 | """ 16 | 17 | extendByDays: Optional[int] = attr.ib(default=None) 18 | """ 19 | The number of days to extend the subscription renewal date. 20 | 21 | https://developer.apple.com/documentation/appstoreserverapi/extendbydays 22 | maximum: 90 23 | """ 24 | 25 | extendReasonCode: Optional[ExtendReasonCode] = attr.ib(default=None) 26 | """ 27 | The reason code for the subscription-renewal-date extension. 28 | 29 | https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode 30 | """ 31 | 32 | requestIdentifier: Optional[str] = attr.ib(default=None) 33 | """ 34 | A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. 35 | 36 | https://developer.apple.com/documentation/appstoreserverapi/requestidentifier 37 | """ 38 | 39 | storefrontCountryCodes: Optional[List[str]] = attr.ib(default=None) 40 | """ 41 | A list of storefront country codes you provide to limit the storefronts for a subscription-renewal-date extension. 42 | 43 | https://developer.apple.com/documentation/appstoreserverapi/storefrontcountrycodes 44 | """ 45 | 46 | productId: Optional[str] = attr.ib(default=None) 47 | """ 48 | The unique identifier for the product, that you create in App Store Connect. 49 | 50 | https://developer.apple.com/documentation/appstoreserverapi/productid 51 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/models/MassExtendRenewalDateStatusResponse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | 4 | from attr import define 5 | import attr 6 | 7 | @define 8 | class MassExtendRenewalDateStatusResponse: 9 | """ 10 | A response that indicates the current status of a request to extend the subscription renewal date to all eligible subscribers. 11 | 12 | https://developer.apple.com/documentation/appstoreserverapi/massextendrenewaldatestatusresponse 13 | """ 14 | 15 | requestIdentifier: Optional[str] = attr.ib(default=None) 16 | """ 17 | A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. 18 | 19 | https://developer.apple.com/documentation/appstoreserverapi/requestidentifier 20 | """ 21 | 22 | complete: Optional[bool] = attr.ib(default=None) 23 | """ 24 | A Boolean value that indicates whether the App Store completed the request to extend a subscription renewal date to active subscribers. 25 | 26 | https://developer.apple.com/documentation/appstoreserverapi/complete 27 | """ 28 | 29 | completeDate: Optional[int] = attr.ib(default=None) 30 | """ 31 | The UNIX time, in milliseconds, that the App Store completes a request to extend a subscription renewal date for eligible subscribers. 32 | 33 | https://developer.apple.com/documentation/appstoreserverapi/completedate 34 | """ 35 | 36 | succeededCount: Optional[int] = attr.ib(default=None) 37 | """ 38 | The count of subscriptions that successfully receive a subscription-renewal-date extension. 39 | 40 | https://developer.apple.com/documentation/appstoreserverapi/succeededcount 41 | """ 42 | 43 | failedCount: Optional[int] = attr.ib(default=None) 44 | """ 45 | The count of subscriptions that fail to receive a subscription-renewal-date extension. 46 | 47 | https://developer.apple.com/documentation/appstoreserverapi/failedcount 48 | """ -------------------------------------------------------------------------------- /tests/resources/xcode/xcode-app-receipt-empty: -------------------------------------------------------------------------------- 1 | MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwGggCSABIHhMYHeMA8CAQACAQEEBwwFWGNvZGUwCwIBAQIBAQQDAgEAMDUCAQICAQEELQwrY29tLmV4YW1wbGUubmF0dXJlbGFiLmJhY2t5YXJkYmlyZHMuZXhhbXBsZTALAgEDAgEBBAMMATEwEAIBBAIBAQQI0bz+zwQAAAAwHAIBBQIBAQQU4nEwK24WxZhKi0PSGTYgWoXOIqMwCgIBCAIBAQQCFgAwHgIBDAIBAQQWFhQyMDIzLTEwLTE5VDAxOjE4OjU0WjAeAgEVAgEBBBYWFDQwMDEtMDEtMDFUMDA6MDA6MDBaAAAAAAAAoIIDeDCCA3QwggJcoAMCAQICAQEwDQYJKoZIhvcNAQELBQAwXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0MB4XDTIwMDQwMTE3NTIzNVoXDTQwMDMyNzE3NTIzNVowXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA23+QPCxzD9uXJkuTuwr4oSE+yGHZJMheH3U+2pPbMRqRgLm/5QzLPLsORGIm+gQptknnb+Ab5g1ozSVuw3YI9UoLrnp0PMSpC7PPYg/7tLz324ReKOtHDfHti6z1n7AJOKNue8smUAoa4YnRcnYLOUzLT27As1+3lbq5qF1KdKvvb0GlfgmNuj09zXBX2O3v1dp3yJMEHO8JiHhlzoHyjXLnBxpuJhL3MrENuziQawbE/A3llVDNkci6JfRYyYzhcdtKRfMtGZYDVoGmRO51d1tTz3isXbo+X1ArXCmM3cLXKhffIrTX5Hior6htp8HaaC1mzM8pC1As48L75l8SwQIDAQABozswOTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIChDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEAsgDgPPHo6WK9wNYdQJ5XuTiQd3ZS0qhLcG64Z5n7s4pVn+8dKLhfKtFznzVHN7tG03YQ8vBp7M1imXH5YIqESDjEvYtnJbmrbDNlrdjCmnhID+nMwScNxs9kPG2AWTOMyjYGKhEbjUnOCP9mwEcoS+tawSsJViylqgkDezIx3OiFeEjOwMUSEWoPDK4vBcpvemR/ICx15kyxEtP94x9eDX24WNegfOR/Y6uXmivDKtjQsuHVWg05G29nKKkSg9aHeG2ZvV6zCuCYzvbqw45taeu3QIE9hz1wUdHEXY2l3H9qWBreYHY3Uuz/rBldDBUvig/1icjXKx0e7CuRBac9TzGCAY8wggGLAgEBMGQwXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0AgEBMA0GCWCGSAFlAwQCAQUAMA0GCSqGSIb3DQEBCwUABIIBAIjP3bmY+TrOM0e8n7PeH3OEies1+spNT1n8om4424n/NyIJ9XRyj1QGxshxh6p2BQuUQV8mkWKpHYQJqPobVEcl72ndbHSfzkH2vM57jy/2bCopLt+zWQl0QMA9iKEB3G075wgyD6lcSveZnER/4J6E9+tO6O3R2YFVziwL2UmNR1XgfOhKyNwCfSV1CyVVoSUkkZI7fJ1S6Pce2nLKM1pf+oCWr5vAySd9E4givt/YagGJF+3RHZMEcrqHnnP8kQKi99xnXcIfYyK6VMD9uBb2+4N7MCRDhoY/8+vX9I75paW0UicS6MwacJPueNxLaAboOP4nFSlYhEhZuLiZrdIAAAAAAAA= -------------------------------------------------------------------------------- /tests/util.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from typing import Any, Dict 4 | import jwt 5 | from jwt.api_jwt import decode_complete 6 | import json 7 | import os 8 | 9 | from cryptography.hazmat.primitives.asymmetric import ec 10 | from cryptography.hazmat.primitives import serialization 11 | from appstoreserverlibrary.models.Environment import Environment 12 | 13 | from appstoreserverlibrary.signed_data_verifier import SignedDataVerifier 14 | 15 | def create_signed_data_from_json(path: str) -> str: 16 | data = read_data_from_file(path) 17 | decoded_data = json.loads(data) 18 | private_key = ec.generate_private_key(ec.SECP256R1()).private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()).decode() 19 | return jwt.encode(payload=decoded_data, key=private_key, algorithm='ES256') 20 | 21 | def decode_json_from_signed_date(data: str) -> Dict[str, Any]: 22 | public_key = ec.generate_private_key(ec.SECP256R1()).public_key().public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo).decode() 23 | return decode_complete(jwt=data, key=public_key, algorithms=['ES256'], options={"verify_signature": False}) 24 | 25 | def read_data_from_file(path: str) -> str: 26 | full_path = os.path.join(path) 27 | with open(full_path, mode='r') as test_file: 28 | return test_file.read() 29 | 30 | def read_data_from_binary_file(path: str) -> str: 31 | full_path = os.path.join(path) 32 | with open(full_path, mode='rb') as test_file: 33 | return test_file.read() 34 | 35 | def get_signed_data_verifier(env: Environment, bundle_id: str, app_apple_id: int = 1234) -> SignedDataVerifier: 36 | verifier = SignedDataVerifier([read_data_from_binary_file('tests/resources/certs/testCA.der')], False, env, bundle_id, app_apple_id) 37 | verifier._chain_verifier.enable_strict_checks = False # We don't have authority identifiers on test certs 38 | return verifier 39 | 40 | def get_default_signed_data_verifier(): 41 | return get_signed_data_verifier(Environment.LOCAL_TESTING, "com.example") -------------------------------------------------------------------------------- /appstoreserverlibrary/models/HistoryResponse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from attr import define 4 | from typing import List, Optional 5 | import attr 6 | 7 | from .Environment import Environment 8 | from .LibraryUtility import AttrsRawValueAware 9 | 10 | @define 11 | class HistoryResponse(AttrsRawValueAware): 12 | """ 13 | A response that contains the customer's transaction history for an app. 14 | 15 | https://developer.apple.com/documentation/appstoreserverapi/historyresponse 16 | """ 17 | 18 | revision: Optional[str] = attr.ib(default=None) 19 | """ 20 | A token you use in a query to request the next set of transactions for the customer. 21 | 22 | https://developer.apple.com/documentation/appstoreserverapi/revision 23 | """ 24 | 25 | hasMore: Optional[bool] = attr.ib(default=None) 26 | """ 27 | A Boolean value indicating whether the App Store has more transaction data. 28 | 29 | https://developer.apple.com/documentation/appstoreserverapi/hasmore 30 | """ 31 | 32 | bundleId: Optional[str] = attr.ib(default=None) 33 | """ 34 | The bundle identifier of an app. 35 | 36 | https://developer.apple.com/documentation/appstoreserverapi/bundleid 37 | """ 38 | 39 | appAppleId: Optional[int] = attr.ib(default=None) 40 | """ 41 | The unique identifier of an app in the App Store. 42 | 43 | https://developer.apple.com/documentation/appstoreservernotifications/appappleid 44 | """ 45 | 46 | environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment') 47 | """ 48 | The server environment in which you're making the request, whether sandbox or production. 49 | 50 | https://developer.apple.com/documentation/appstoreserverapi/environment 51 | """ 52 | 53 | rawEnvironment: Optional[str] = Environment.create_raw_attr('environment') 54 | """ 55 | See environment 56 | """ 57 | 58 | signedTransactions: Optional[List[str]] = attr.ib(default=None) 59 | """ 60 | An array of in-app purchase transactions for the customer, signed by Apple, in JSON Web Signature format. 61 | 62 | https://developer.apple.com/documentation/appstoreserverapi/jwstransaction 63 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/promotional_offer.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from cryptography.hazmat.primitives import serialization 4 | from cryptography.hazmat.backends import default_backend 5 | from cryptography.hazmat.primitives.hashes import SHA256 6 | from cryptography.hazmat.primitives.asymmetric.ec import ECDSA, EllipticCurvePrivateKey 7 | 8 | import uuid 9 | import base64 10 | 11 | class PromotionalOfferSignatureCreator: 12 | _signing_key: EllipticCurvePrivateKey 13 | _key_id: str 14 | _bundle_id: str 15 | def __init__(self, signing_key: bytes, key_id: str, bundle_id: str): 16 | self._signing_key = serialization.load_pem_private_key(signing_key, password=None, backend=default_backend()) 17 | self._key_id = key_id 18 | self._bundle_id = bundle_id 19 | def create_signature(self, product_identifier: str, subscription_offer_id: str, application_username: str, nonce: uuid.UUID, timestamp: int): 20 | """ 21 | Return the Base64 encoded signature 22 | 23 | https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers 24 | 25 | :param product_identifier: The subscription product identifier 26 | :param subscription_offer_id: The subscription discount identifier 27 | :param application_username: An optional string value that you define; may be an empty string 28 | :param nonce: A one-time UUID value that your server generates. Generate a new nonce for every signature. 29 | :param timestamp: A timestamp your server generates in UNIX time format, in milliseconds. The timestamp keeps the offer active for 24 hours. 30 | :return: The Base64 encoded signature 31 | """ 32 | payload = self._bundle_id + '\u2063' + \ 33 | self._key_id + '\u2063' + \ 34 | product_identifier + '\u2063' + \ 35 | subscription_offer_id + '\u2063' + \ 36 | application_username.lower() + '\u2063'+ \ 37 | str(nonce).lower() + '\u2063' + \ 38 | str(timestamp) 39 | 40 | return base64.b64encode(self._signing_key.sign( 41 | payload.encode('utf-8'), ECDSA(SHA256()) 42 | )).decode('utf-8') 43 | -------------------------------------------------------------------------------- /tests/resources/xcode/xcode-app-receipt-with-transaction: -------------------------------------------------------------------------------- 1 | MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwGggCSABIIBdjGCAXIwDwIBAAIBAQQHDAVYY29kZTALAgEBAgEBBAMCAQAwNQIBAgIBAQQtDCtjb20uZXhhbXBsZS5uYXR1cmVsYWIuYmFja3lhcmRiaXJkcy5leGFtcGxlMAsCAQMCAQEEAwwBMTAQAgEEAgEBBAjyv/X7DwAAADAcAgEFAgEBBBQWU6vLoHZxeVVlaOg/UEG2OOKahTAKAgEIAgEBBAIWADAeAgEMAgEBBBYWFDIwMjMtMTAtMTlUMDE6NDU6NDBaMIGRAgERAgEBBIGIMYGFMAwCAgalAgEBBAMCAQEwFwICBqYCAQEEDgwMcGFzcy5wcmVtaXVtMAwCAganAgEBBAMMATAwHwICBqgCAQEEFhYUMjAyMy0xMC0xOVQwMTo0NTozNlowHwICBqwCAQEEFhYUMjAyMy0xMS0xOVQwMTo0NTozNlowDAICBrcCAQEEAwIBATAeAgEVAgEBBBYWFDQwMDEtMDEtMDFUMDA6MDA6MDBaAAAAAAAAoIIDeDCCA3QwggJcoAMCAQICAQEwDQYJKoZIhvcNAQELBQAwXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0MB4XDTIwMDQwMTE3NTIzNVoXDTQwMDMyNzE3NTIzNVowXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA23+QPCxzD9uXJkuTuwr4oSE+yGHZJMheH3U+2pPbMRqRgLm/5QzLPLsORGIm+gQptknnb+Ab5g1ozSVuw3YI9UoLrnp0PMSpC7PPYg/7tLz324ReKOtHDfHti6z1n7AJOKNue8smUAoa4YnRcnYLOUzLT27As1+3lbq5qF1KdKvvb0GlfgmNuj09zXBX2O3v1dp3yJMEHO8JiHhlzoHyjXLnBxpuJhL3MrENuziQawbE/A3llVDNkci6JfRYyYzhcdtKRfMtGZYDVoGmRO51d1tTz3isXbo+X1ArXCmM3cLXKhffIrTX5Hior6htp8HaaC1mzM8pC1As48L75l8SwQIDAQABozswOTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIChDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEAsgDgPPHo6WK9wNYdQJ5XuTiQd3ZS0qhLcG64Z5n7s4pVn+8dKLhfKtFznzVHN7tG03YQ8vBp7M1imXH5YIqESDjEvYtnJbmrbDNlrdjCmnhID+nMwScNxs9kPG2AWTOMyjYGKhEbjUnOCP9mwEcoS+tawSsJViylqgkDezIx3OiFeEjOwMUSEWoPDK4vBcpvemR/ICx15kyxEtP94x9eDX24WNegfOR/Y6uXmivDKtjQsuHVWg05G29nKKkSg9aHeG2ZvV6zCuCYzvbqw45taeu3QIE9hz1wUdHEXY2l3H9qWBreYHY3Uuz/rBldDBUvig/1icjXKx0e7CuRBac9TzGCAY8wggGLAgEBMGQwXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0AgEBMA0GCWCGSAFlAwQCAQUAMA0GCSqGSIb3DQEBCwUABIIBAMNY9TpOCg59NnKdDA6Xc4D74lEaa+YwQqD/z8ajAGxpw3efoQRvx8Q1qR6IVs9BcRYGyJmsFrau19QeSIRjjqaxhV8ZbRFenWp0Yps6OCPVHw94Ej3AstAL/8WIArBM1OS6OZJESJdQz5xpwavWLGm1rU2730glMdHzHfm2h0wNp/0BKV0ugV9SRQN4RsyAMNS+rCO1mtSDI6nx8E+dEVMIa4mUg+yhXRlg6KzdzKWnr9vDtRVmhdq0ANfP+jfvncsyC+d/c3cAsXOK066hKFwYWTKaRZ7M2eXus5TcU83/aaovHyKVyKKCRnKuP7VPt9d5eWLSg/7v2ctHJtjmhqsAAAAAAAA= -------------------------------------------------------------------------------- /tests/resources/mock_signed_data/transactionInfo: -------------------------------------------------------------------------------- 1 | eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJDekFLQmdncWhrak9QUVFEQWpCTk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1SVXdFd1lEVlFRS0RBeEpiblJsY20xbFpHbGhkR1V3SGhjTk1qTXdNVEEwTVRZek56TXhXaGNOTXpJeE1qTXhNVFl6TnpNeFdqQkZNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRM1Z3WlhKMGFXNXZNUTB3Q3dZRFZRUUtEQVJNWldGbU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTRyV0J4R21GYm5QSVBRSTB6c0JLekx4c2o4cEQydnFicjB5UElTVXgyV1F5eG1yTnFsOWZoSzhZRUV5WUZWNysrcDVpNFlVU1Ivbzl1UUlnQ1BJaHJLTWZNQjB3Q1FZRFZSMFRCQUl3QURBUUJnb3Foa2lHOTJOa0Jnc0JCQUlUQURBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlFQWtpRVprb0ZNa2o0Z1huK1E5alhRWk1qWjJnbmpaM2FNOE5ZcmdmVFVpdlFDSURKWVowRmFMZTduU0lVMkxXTFRrNXRYVENjNEU4R0pTWWYvc1lSeEVGaWUiLCJNSUlCbHpDQ0FUMmdBd0lCQWdJQkJqQUtCZ2dxaGtqT1BRUURBakEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qWXdNVm9YRFRNeU1USXpNVEUyTWpZd01Wb3dUVEVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekVWTUJNR0ExVUVDZ3dNU1c1MFpYSnRaV1JwWVhSbE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUZRM2xYMnNxTjlHSXdBaWlNUURRQy9reW5TZ1g0N1J3dmlET3RNWFh2eUtkUWU2Q1BzUzNqbzJ1UkR1RXFBeFdlT2lDcmpsRFdzeXo1d3dkVTBndGFxTWxNQ013RHdZRFZSMFRCQWd3QmdFQi93SUJBREFRQmdvcWhraUc5Mk5rQmdJQkJBSVRBREFLQmdncWhrak9QUVFEQWdOSUFEQkZBaUVBdm56TWNWMjY4Y1JiMS9GcHlWMUVoVDNXRnZPenJCVVdQNi9Ub1RoRmF2TUNJRmJhNXQ2WUt5MFIySkR0eHF0T2pKeTY2bDZWN2QvUHJBRE5wa21JUFcraSIsIk1JSUJYRENDQVFJQ0NRQ2ZqVFVHTERuUjlqQUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qQXpNbG9YRFRNek1ERXdNVEUyTWpBek1sb3dOakVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCSFB2d1pmb0tMS2FPclgvV2U0cU9iWFNuYTVUZFdIVlo2aElSQTF3MG9jM1FDVDBJbzJwbHlEQjMvTVZsazJ0YzRLR0U4VGlxVzdpYlE2WmM5VjY0azB3Q2dZSUtvWkl6ajBFQXdNRFNBQXdSUUloQU1USGhXdGJBUU4waFN4SVhjUDRDS3JEQ0gvZ3N4V3B4NmpUWkxUZVorRlBBaUIzNW53azVxMHpjSXBlZnZZSjBNVS95R0dIU1dlejBicTBwRFlVTy9ubUR3PT0iXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJlbnZpcm9ubWVudCI6IlNhbmRib3giLCJidW5kbGVJZCI6ImNvbS5leGFtcGxlIiwic2lnbmVkRGF0ZSI6MTY3Mjk1NjE1NDAwMH0.PnHWpeIJZ8f2Q218NSGLo_aR0IBEJvC6PxmxKXh-qfYTrZccx2suGl223OSNAX78e4Ylf2yJCG2N-FfU-NIhZQ -------------------------------------------------------------------------------- /tests/resources/mock_signed_data/renewalInfo: -------------------------------------------------------------------------------- 1 | eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJEREFLQmdncWhrak9QUVFEQXpCRk1Rc3dDUVlEVlFRR0V3SlZVekVMTUFrR0ExVUVDQXdDUTBFeEVqQVFCZ05WQkFjTUNVTjFjR1Z5ZEdsdWJ6RVZNQk1HQTFVRUNnd01TVzUwWlhKdFpXUnBZWFJsTUI0WERUSXpNREV3TlRJeE16RXpORm9YRFRNek1ERXdNVEl4TXpFek5Gb3dQVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RFRBTEJnTlZCQW9NQkV4bFlXWXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVGl0WUhFYVlWdWM4ZzlBalRPd0VyTXZHeVB5a1BhK3B1dlRJOGhKVEhaWkRMR2FzMnFYMStFcnhnUVRKZ1ZYdjc2bm1MaGhSSkgrajI1QWlBSThpR3NveTh3TFRBSkJnTlZIUk1FQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3TklBREJGQWlCWDRjK1QwRnA1bko1UVJDbFJmdTVQU0J5UnZOUHR1YVRzazB2UEIzV0FJQUloQU5nYWF1QWovWVA5czBBa0VoeUpoeFFPLzZRMnpvdVorSDFDSU9laG5NelEiLCJNSUlCbnpDQ0FVV2dBd0lCQWdJQkN6QUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TlRJeE16RXdOVm9YRFRNek1ERXdNVEl4TXpFd05Wb3dSVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RlRBVEJnTlZCQW9NREVsdWRHVnliV1ZrYVdGMFpUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJCVU41VjlyS2pmUmlNQUlvakVBMEF2NU1wMG9GK08wY0w0Z3pyVEYxNzhpblVIdWdqN0V0NDZOcmtRN2hLZ01WbmpvZ3E0NVExck1zK2NNSFZOSUxXcWpOVEF6TUE4R0ExVWRFd1FJTUFZQkFmOENBUUF3RGdZRFZSMFBBUUgvQkFRREFnRUdNQkFHQ2lxR1NJYjNZMlFHQWdFRUFnVUFNQW9HQ0NxR1NNNDlCQU1EQTBnQU1FVUNJUUNtc0lLWXM0MXVsbHNzSFg0clZ2ZVVUMFo3SXM1L2hMSzFsRlBUdHVuM2hBSWdjMisyUkc1K2dOY0ZWY3MrWEplRWw0R1orb2psM1JPT21sbCt5ZTdkeW5RPSIsIk1JSUJnakNDQVNtZ0F3SUJBZ0lKQUxVYzVBTGlINXBiTUFvR0NDcUdTTTQ5QkFNRE1EWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERkWEJsY25ScGJtOHdIaGNOTWpNd01UQTFNakV6TURJeVdoY05Nek13TVRBeU1qRXpNREl5V2pBMk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWMrL0JsK2dvc3BvNnRmOVo3aW81dGRLZHJsTjFZZFZucUVoRURYRFNoemRBSlBRaWphbVhJTUhmOHhXV1RhMXpnb1lUeE9LcGJ1SnREcGx6MVhyaVRhTWdNQjR3REFZRFZSMFRCQVV3QXdFQi96QU9CZ05WSFE4QkFmOEVCQU1DQVFZd0NnWUlLb1pJemowRUF3TURSd0F3UkFJZ2VtV1FYbk1BZFRhZDJKREpXbmc5VTR1QkJMNW1BN1dJMDVIN29IN2M2aVFDSUhpUnFNak5melVBeWl1OWg2ck9VL0sraVRSMEkvM1kvTlNXc1hIWCthY2MiXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJlbnZpcm9ubWVudCI6IlNhbmRib3giLCJzaWduZWREYXRlIjoxNjcyOTU2MTU0MDAwfQ.FbK2OL-t6l4892W7fzWyus_g9mIl2CzWLbVt7Kgcnt6zzVulF8bzovgpe0v_y490blROGixy8KDoe2dSU53-Xw -------------------------------------------------------------------------------- /appstoreserverlibrary/models/DecodedRealtimeRequestBody.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | from typing import Optional 4 | from uuid import UUID 5 | 6 | from attr import define 7 | import attr 8 | 9 | from .Environment import Environment 10 | from .LibraryUtility import AttrsRawValueAware 11 | 12 | @define 13 | class DecodedRealtimeRequestBody(AttrsRawValueAware): 14 | """ 15 | The decoded request body the App Store sends to your server to request a real-time retention message. 16 | 17 | https://developer.apple.com/documentation/retentionmessaging/decodedrealtimerequestbody 18 | """ 19 | 20 | originalTransactionId: str = attr.ib() 21 | """ 22 | The original transaction identifier of the customer's subscription. 23 | 24 | https://developer.apple.com/documentation/retentionmessaging/originaltransactionid 25 | """ 26 | 27 | appAppleId: int = attr.ib() 28 | """ 29 | The unique identifier of the app in the App Store. 30 | 31 | https://developer.apple.com/documentation/retentionmessaging/appappleid 32 | """ 33 | 34 | productId: str = attr.ib() 35 | """ 36 | The unique identifier of the auto-renewable subscription. 37 | 38 | https://developer.apple.com/documentation/retentionmessaging/productid 39 | """ 40 | 41 | userLocale: str = attr.ib() 42 | """ 43 | The device's locale. 44 | 45 | https://developer.apple.com/documentation/retentionmessaging/locale 46 | """ 47 | 48 | requestIdentifier: UUID = attr.ib() 49 | """ 50 | A UUID the App Store server creates to uniquely identify each request. 51 | 52 | https://developer.apple.com/documentation/retentionmessaging/requestidentifier 53 | """ 54 | 55 | signedDate: int = attr.ib() 56 | """ 57 | The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature (JWS) data. 58 | 59 | https://developer.apple.com/documentation/retentionmessaging/signeddate 60 | """ 61 | 62 | environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment', raw_required=True) 63 | """ 64 | The server environment, either sandbox or production. 65 | 66 | https://developer.apple.com/documentation/retentionmessaging/environment 67 | """ 68 | 69 | rawEnvironment: str = Environment.create_raw_attr('environment', required=True) 70 | """ 71 | See environment 72 | """ 73 | 74 | -------------------------------------------------------------------------------- /tests/resources/mock_signed_data/wrongBundleId: -------------------------------------------------------------------------------- 1 | eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJEREFLQmdncWhrak9QUVFEQXpCRk1Rc3dDUVlEVlFRR0V3SlZVekVMTUFrR0ExVUVDQXdDUTBFeEVqQVFCZ05WQkFjTUNVTjFjR1Z5ZEdsdWJ6RVZNQk1HQTFVRUNnd01TVzUwWlhKdFpXUnBZWFJsTUI0WERUSXpNREV3TlRJeE16RXpORm9YRFRNek1ERXdNVEl4TXpFek5Gb3dQVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RFRBTEJnTlZCQW9NQkV4bFlXWXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVGl0WUhFYVlWdWM4ZzlBalRPd0VyTXZHeVB5a1BhK3B1dlRJOGhKVEhaWkRMR2FzMnFYMStFcnhnUVRKZ1ZYdjc2bm1MaGhSSkgrajI1QWlBSThpR3NveTh3TFRBSkJnTlZIUk1FQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3TklBREJGQWlCWDRjK1QwRnA1bko1UVJDbFJmdTVQU0J5UnZOUHR1YVRzazB2UEIzV0FJQUloQU5nYWF1QWovWVA5czBBa0VoeUpoeFFPLzZRMnpvdVorSDFDSU9laG5NelEiLCJNSUlCbnpDQ0FVV2dBd0lCQWdJQkN6QUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TlRJeE16RXdOVm9YRFRNek1ERXdNVEl4TXpFd05Wb3dSVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RlRBVEJnTlZCQW9NREVsdWRHVnliV1ZrYVdGMFpUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJCVU41VjlyS2pmUmlNQUlvakVBMEF2NU1wMG9GK08wY0w0Z3pyVEYxNzhpblVIdWdqN0V0NDZOcmtRN2hLZ01WbmpvZ3E0NVExck1zK2NNSFZOSUxXcWpOVEF6TUE4R0ExVWRFd1FJTUFZQkFmOENBUUF3RGdZRFZSMFBBUUgvQkFRREFnRUdNQkFHQ2lxR1NJYjNZMlFHQWdFRUFnVUFNQW9HQ0NxR1NNNDlCQU1EQTBnQU1FVUNJUUNtc0lLWXM0MXVsbHNzSFg0clZ2ZVVUMFo3SXM1L2hMSzFsRlBUdHVuM2hBSWdjMisyUkc1K2dOY0ZWY3MrWEplRWw0R1orb2psM1JPT21sbCt5ZTdkeW5RPSIsIk1JSUJnakNDQVNtZ0F3SUJBZ0lKQUxVYzVBTGlINXBiTUFvR0NDcUdTTTQ5QkFNRE1EWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERkWEJsY25ScGJtOHdIaGNOTWpNd01UQTFNakV6TURJeVdoY05Nek13TVRBeU1qRXpNREl5V2pBMk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWMrL0JsK2dvc3BvNnRmOVo3aW81dGRLZHJsTjFZZFZucUVoRURYRFNoemRBSlBRaWphbVhJTUhmOHhXV1RhMXpnb1lUeE9LcGJ1SnREcGx6MVhyaVRhTWdNQjR3REFZRFZSMFRCQVV3QXdFQi96QU9CZ05WSFE4QkFmOEVCQU1DQVFZd0NnWUlLb1pJemowRUF3TURSd0F3UkFJZ2VtV1FYbk1BZFRhZDJKREpXbmc5VTR1QkJMNW1BN1dJMDVIN29IN2M2aVFDSUhpUnFNak5melVBeWl1OWg2ck9VL0sraVRSMEkvM1kvTlNXc1hIWCthY2MiXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJkYXRhIjp7ImJ1bmRsZUlkIjoiY29tLmV4YW1wbGUud3JvbmcifSwibm90aWZpY2F0aW9uVVVJRCI6IjlhZDU2YmQyLTBiYzYtNDJlMC1hZjI0LWZkOTk2ZDg3YTFlNiIsIm5vdGlmaWNhdGlvblR5cGUiOiJURVNUIn0.WWE31hTB_mcv2O_lf-xI-MNY3d8txc0MzpqFx4QnYDfFIxB95Lo2Fm3r46YSjLLdL7xCWdEJrJP5bHgRCejAGg -------------------------------------------------------------------------------- /tests/resources/mock_signed_data/missingX5CHeaderClaim: -------------------------------------------------------------------------------- 1 | eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsIng1Y3dyb25nIjpbIk1JSUJvRENDQVVhZ0F3SUJBZ0lCRERBS0JnZ3Foa2pPUFFRREF6QkZNUXN3Q1FZRFZRUUdFd0pWVXpFTE1Ba0dBMVVFQ0F3Q1EwRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekVWTUJNR0ExVUVDZ3dNU1c1MFpYSnRaV1JwWVhSbE1CNFhEVEl6TURFd05USXhNekV6TkZvWERUTXpNREV3TVRJeE16RXpORm93UFRFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba05CTVJJd0VBWURWUVFIREFsRGRYQmxjblJwYm04eERUQUxCZ05WQkFvTUJFeGxZV1l3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVRpdFlIRWFZVnVjOGc5QWpUT3dFck12R3lQeWtQYStwdXZUSThoSlRIWlpETEdhczJxWDErRXJ4Z1FUSmdWWHY3Nm5tTGhoUkpIK2oyNUFpQUk4aUdzb3k4d0xUQUpCZ05WSFJNRUFqQUFNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVFCZ29xaGtpRzkyTmtCZ3NCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05JQURCRkFpQlg0YytUMEZwNW5KNVFSQ2xSZnU1UFNCeVJ2TlB0dWFUc2swdlBCM1dBSUFJaEFOZ2FhdUFqL1lQOXMwQWtFaHlKaHhRTy82UTJ6b3VaK0gxQ0lPZWhuTXpRIiwiTUlJQm56Q0NBVVdnQXdJQkFnSUJDekFLQmdncWhrak9QUVFEQXpBMk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1CNFhEVEl6TURFd05USXhNekV3TlZvWERUTXpNREV3TVRJeE16RXdOVm93UlRFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba05CTVJJd0VBWURWUVFIREFsRGRYQmxjblJwYm04eEZUQVRCZ05WQkFvTURFbHVkR1Z5YldWa2FXRjBaVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCQlVONVY5cktqZlJpTUFJb2pFQTBBdjVNcDBvRitPMGNMNGd6clRGMTc4aW5VSHVnajdFdDQ2TnJrUTdoS2dNVm5qb2dxNDVRMXJNcytjTUhWTklMV3FqTlRBek1BOEdBMVVkRXdRSU1BWUJBZjhDQVFBd0RnWURWUjBQQVFIL0JBUURBZ0VHTUJBR0NpcUdTSWIzWTJRR0FnRUVBZ1VBTUFvR0NDcUdTTTQ5QkFNREEwZ0FNRVVDSVFDbXNJS1lzNDF1bGxzc0hYNHJWdmVVVDBaN0lzNS9oTEsxbEZQVHR1bjNoQUlnYzIrMlJHNStnTmNGVmNzK1hKZUVsNEdaK29qbDNST09tbGwreWU3ZHluUT0iLCJNSUlCZ2pDQ0FTbWdBd0lCQWdJSkFMVWM1QUxpSDVwYk1Bb0dDQ3FHU000OUJBTURNRFl4Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh3SGhjTk1qTXdNVEExTWpFek1ESXlXaGNOTXpNd01UQXlNakV6TURJeVdqQTJNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRM1Z3WlhKMGFXNXZNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVjKy9CbCtnb3NwbzZ0ZjlaN2lvNXRkS2RybE4xWWRWbnFFaEVEWERTaHpkQUpQUWlqYW1YSU1IZjh4V1dUYTF6Z29ZVHhPS3BidUp0RHBsejFYcmlUYU1nTUI0d0RBWURWUjBUQkFVd0F3RUIvekFPQmdOVkhROEJBZjhFQkFNQ0FRWXdDZ1lJS29aSXpqMEVBd01EUndBd1JBSWdlbVdRWG5NQWRUYWQySkRKV25nOVU0dUJCTDVtQTdXSTA1SDdvSDdjNmlRQ0lIaVJxTWpOZnpVQXlpdTloNnJPVS9LK2lUUjBJLzNZL05TV3NYSFgrYWNjIl19.eyJkYXRhIjp7ImJ1bmRsZUlkIjoiY29tLmV4YW1wbGUifSwibm90aWZpY2F0aW9uVVVJRCI6IjlhZDU2YmQyLTBiYzYtNDJlMC1hZjI0LWZkOTk2ZDg3YTFlNiIsIm5vdGlmaWNhdGlvblR5cGUiOiJURVNUIn0.1TFhjDR4WwQJNgizVGYXz3WE3ajxTdH1wKLQQ71MtrkadSxxOo3yPo_6L9Z03unIU7YK-NRNzSIb5bh5WqTprQ -------------------------------------------------------------------------------- /tests/resources/mock_signed_data/testNotification: -------------------------------------------------------------------------------- 1 | eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJDekFLQmdncWhrak9QUVFEQWpCTk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1SVXdFd1lEVlFRS0RBeEpiblJsY20xbFpHbGhkR1V3SGhjTk1qTXdNVEEwTVRZek56TXhXaGNOTXpJeE1qTXhNVFl6TnpNeFdqQkZNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRM1Z3WlhKMGFXNXZNUTB3Q3dZRFZRUUtEQVJNWldGbU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTRyV0J4R21GYm5QSVBRSTB6c0JLekx4c2o4cEQydnFicjB5UElTVXgyV1F5eG1yTnFsOWZoSzhZRUV5WUZWNysrcDVpNFlVU1Ivbzl1UUlnQ1BJaHJLTWZNQjB3Q1FZRFZSMFRCQUl3QURBUUJnb3Foa2lHOTJOa0Jnc0JCQUlUQURBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlFQWtpRVprb0ZNa2o0Z1huK1E5alhRWk1qWjJnbmpaM2FNOE5ZcmdmVFVpdlFDSURKWVowRmFMZTduU0lVMkxXTFRrNXRYVENjNEU4R0pTWWYvc1lSeEVGaWUiLCJNSUlCbHpDQ0FUMmdBd0lCQWdJQkJqQUtCZ2dxaGtqT1BRUURBakEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qWXdNVm9YRFRNeU1USXpNVEUyTWpZd01Wb3dUVEVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekVWTUJNR0ExVUVDZ3dNU1c1MFpYSnRaV1JwWVhSbE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUZRM2xYMnNxTjlHSXdBaWlNUURRQy9reW5TZ1g0N1J3dmlET3RNWFh2eUtkUWU2Q1BzUzNqbzJ1UkR1RXFBeFdlT2lDcmpsRFdzeXo1d3dkVTBndGFxTWxNQ013RHdZRFZSMFRCQWd3QmdFQi93SUJBREFRQmdvcWhraUc5Mk5rQmdJQkJBSVRBREFLQmdncWhrak9QUVFEQWdOSUFEQkZBaUVBdm56TWNWMjY4Y1JiMS9GcHlWMUVoVDNXRnZPenJCVVdQNi9Ub1RoRmF2TUNJRmJhNXQ2WUt5MFIySkR0eHF0T2pKeTY2bDZWN2QvUHJBRE5wa21JUFcraSIsIk1JSUJYRENDQVFJQ0NRQ2ZqVFVHTERuUjlqQUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qQXpNbG9YRFRNek1ERXdNVEUyTWpBek1sb3dOakVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCSFB2d1pmb0tMS2FPclgvV2U0cU9iWFNuYTVUZFdIVlo2aElSQTF3MG9jM1FDVDBJbzJwbHlEQjMvTVZsazJ0YzRLR0U4VGlxVzdpYlE2WmM5VjY0azB3Q2dZSUtvWkl6ajBFQXdNRFNBQXdSUUloQU1USGhXdGJBUU4waFN4SVhjUDRDS3JEQ0gvZ3N4V3B4NmpUWkxUZVorRlBBaUIzNW53azVxMHpjSXBlZnZZSjBNVS95R0dIU1dlejBicTBwRFlVTy9ubUR3PT0iXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJkYXRhIjp7ImFwcEFwcGxlSWQiOjEyMzQsImVudmlyb25tZW50IjoiU2FuZGJveCIsImJ1bmRsZUlkIjoiY29tLmV4YW1wbGUifSwibm90aWZpY2F0aW9uVVVJRCI6IjlhZDU2YmQyLTBiYzYtNDJlMC1hZjI0LWZkOTk2ZDg3YTFlNiIsInNpZ25lZERhdGUiOjE2ODEzMTQzMjQwMDAsIm5vdGlmaWNhdGlvblR5cGUiOiJURVNUIn0.VVXYwuNm2Y3XsOUva-BozqatRCsDuykA7xIe_CCRw6aIAAxJ1nb2sw871jfZ6dcgNhUuhoZ93hfbc1v_5zB7Og -------------------------------------------------------------------------------- /appstoreserverlibrary/models/Summary.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from attr import define 4 | from typing import List, Optional 5 | import attr 6 | 7 | from .Environment import Environment 8 | from .LibraryUtility import AttrsRawValueAware 9 | 10 | @define 11 | class Summary(AttrsRawValueAware): 12 | """ 13 | The payload data for a subscription-renewal-date extension notification. 14 | 15 | https://developer.apple.com/documentation/appstoreservernotifications/summary 16 | """ 17 | 18 | environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment') 19 | """ 20 | The server environment that the notification applies to, either sandbox or production. 21 | 22 | https://developer.apple.com/documentation/appstoreservernotifications/environment 23 | """ 24 | 25 | rawEnvironment: Optional[str] = Environment.create_raw_attr('environment') 26 | """ 27 | See environment 28 | """ 29 | 30 | appAppleId: Optional[int] = attr.ib(default=None) 31 | """ 32 | The unique identifier of an app in the App Store. 33 | 34 | https://developer.apple.com/documentation/appstoreservernotifications/appappleid 35 | """ 36 | 37 | bundleId: Optional[str] = attr.ib(default=None) 38 | """ 39 | The bundle identifier of an app. 40 | 41 | https://developer.apple.com/documentation/appstoreserverapi/bundleid 42 | """ 43 | 44 | productId: Optional[str] = attr.ib(default=None) 45 | """ 46 | The unique identifier for the product, that you create in App Store Connect. 47 | 48 | https://developer.apple.com/documentation/appstoreserverapi/productid 49 | """ 50 | 51 | requestIdentifier: Optional[str] = attr.ib(default=None) 52 | """ 53 | A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. 54 | 55 | https://developer.apple.com/documentation/appstoreserverapi/requestidentifier 56 | """ 57 | 58 | storefrontCountryCodes: Optional[List[str]] = attr.ib(default=None) 59 | """ 60 | A list of storefront country codes you provide to limit the storefronts for a subscription-renewal-date extension. 61 | 62 | https://developer.apple.com/documentation/appstoreserverapi/storefrontcountrycodes 63 | """ 64 | 65 | succeededCount: Optional[int] = attr.ib(default=None) 66 | """ 67 | The count of subscriptions that successfully receive a subscription-renewal-date extension. 68 | 69 | https://developer.apple.com/documentation/appstoreserverapi/succeededcount 70 | """ 71 | 72 | failedCount: Optional[int] = attr.ib(default=None) 73 | """ 74 | The count of subscriptions that fail to receive a subscription-renewal-date extension. 75 | 76 | https://developer.apple.com/documentation/appstoreserverapi/failedcount 77 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/models/NotificationHistoryRequest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from typing import Optional 4 | from attr import define 5 | import attr 6 | 7 | from .NotificationTypeV2 import NotificationTypeV2 8 | from .Subtype import Subtype 9 | 10 | @define 11 | class NotificationHistoryRequest: 12 | """ 13 | The request body for notification history. 14 | 15 | https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryrequest 16 | """ 17 | 18 | startDate: Optional[int] = attr.ib(default=None) 19 | """ 20 | The start date of the timespan for the requested App Store Server Notification history records. The startDate needs to precede the endDate. Choose a startDate that's within the past 180 days from the current date. 21 | 22 | https://developer.apple.com/documentation/appstoreserverapi/startdate 23 | """ 24 | 25 | endDate: Optional[int] = attr.ib(default=None) 26 | """ 27 | The end date of the timespan for the requested App Store Server Notification history records. Choose an endDate that's later than the startDate. If you choose an endDate in the future, the endpoint automatically uses the current date as the endDate. 28 | 29 | https://developer.apple.com/documentation/appstoreserverapi/enddate 30 | """ 31 | 32 | notificationType: Optional[NotificationTypeV2] = attr.ib(default=None) 33 | """ 34 | A notification type. Provide this field to limit the notification history records to those with this one notification type. For a list of notifications types, see notificationType. 35 | Include either the transactionId or the notificationType in your query, but not both. 36 | 37 | https://developer.apple.com/documentation/appstoreserverapi/notificationtype 38 | """ 39 | 40 | notificationSubtype: Optional[Subtype] = attr.ib(default=None) 41 | """ 42 | A notification subtype. Provide this field to limit the notification history records to those with this one notification subtype. For a list of subtypes, see subtype. If you specify a notificationSubtype, you need to also specify its related notificationType. 43 | 44 | https://developer.apple.com/documentation/appstoreserverapi/notificationsubtype 45 | """ 46 | 47 | transactionId: Optional[str] = attr.ib(default=None) 48 | """ 49 | The transaction identifier, which may be an original transaction identifier, of any transaction belonging to the customer. Provide this field to limit the notification history request to this one customer. 50 | Include either the transactionId or the notificationType in your query, but not both. 51 | 52 | https://developer.apple.com/documentation/appstoreserverapi/transactionid 53 | """ 54 | 55 | onlyFailures: Optional[bool] = attr.ib(default=None) 56 | """ 57 | A Boolean value you set to true to request only the notifications that haven’t reached your server successfully. The response also includes notifications that the App Store server is currently retrying to send to your server. 58 | 59 | https://developer.apple.com/documentation/appstoreserverapi/onlyfailures 60 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/models/Data.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | 4 | from attr import define 5 | import attr 6 | 7 | from .ConsumptionRequestReason import ConsumptionRequestReason 8 | from .Environment import Environment 9 | from .Status import Status 10 | from .LibraryUtility import AttrsRawValueAware 11 | 12 | @define 13 | class Data(AttrsRawValueAware): 14 | """ 15 | The app metadata and the signed renewal and transaction information. 16 | 17 | https://developer.apple.com/documentation/appstoreservernotifications/data 18 | """ 19 | environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment') 20 | """ 21 | The server environment that the notification applies to, either sandbox or production. 22 | 23 | https://developer.apple.com/documentation/appstoreservernotifications/environment 24 | """ 25 | rawEnvironment: Optional[str] = Environment.create_raw_attr('environment') 26 | """ 27 | See environment 28 | """ 29 | 30 | appAppleId: Optional[int] = attr.ib(default=None) 31 | """ 32 | The unique identifier of an app in the App Store. 33 | 34 | https://developer.apple.com/documentation/appstoreservernotifications/appappleid 35 | """ 36 | 37 | bundleId: Optional[str] = attr.ib(default=None) 38 | """ 39 | The bundle identifier of an app. 40 | 41 | https://developer.apple.com/documentation/appstoreserverapi/bundleid 42 | """ 43 | 44 | bundleVersion: Optional[str] = attr.ib(default=None) 45 | """ 46 | The version of the build that identifies an iteration of the bundle. 47 | 48 | https://developer.apple.com/documentation/appstoreservernotifications/bundleversion 49 | """ 50 | 51 | signedTransactionInfo: Optional[str] = attr.ib(default=None) 52 | """ 53 | Transaction information signed by the App Store, in JSON Web Signature (JWS) format. 54 | 55 | https://developer.apple.com/documentation/appstoreserverapi/jwstransaction 56 | """ 57 | 58 | signedRenewalInfo: Optional[str] = attr.ib(default=None) 59 | """ 60 | Subscription renewal information, signed by the App Store, in JSON Web Signature (JWS) format. 61 | 62 | https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo 63 | """ 64 | 65 | status: Optional[Status] = Status.create_main_attr('rawStatus') 66 | """ 67 | The status of an auto-renewable subscription as of the signedDate in the responseBodyV2DecodedPayload. 68 | 69 | https://developer.apple.com/documentation/appstoreservernotifications/status 70 | """ 71 | 72 | rawStatus: Optional[int] = Status.create_raw_attr('status') 73 | """ 74 | See status 75 | """ 76 | 77 | consumptionRequestReason: Optional[ConsumptionRequestReason] = ConsumptionRequestReason.create_main_attr('rawConsumptionRequestReason') 78 | """ 79 | The reason the customer requested the refund. 80 | 81 | https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason 82 | """ 83 | 84 | rawConsumptionRequestReason: Optional[str] = ConsumptionRequestReason.create_raw_attr('consumptionRequestReason') 85 | """ 86 | See consumptionRequestReason 87 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/models/TransactionHistoryRequest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import Enum 4 | from typing import List, Optional 5 | import attr 6 | 7 | 8 | from .InAppOwnershipType import InAppOwnershipType 9 | 10 | class ProductType(str, Enum): 11 | AUTO_RENEWABLE = "AUTO_RENEWABLE" 12 | NON_RENEWABLE = "NON_RENEWABLE" 13 | CONSUMABLE = "CONSUMABLE" 14 | NON_CONSUMABLE = "NON_CONSUMABLE" 15 | 16 | class Order(str, Enum): 17 | ASCENDING = "ASCENDING" 18 | DESCENDING = "DESCENDING" 19 | 20 | @attr.define 21 | class TransactionHistoryRequest: 22 | startDate: Optional[int] = attr.ib(default=None) 23 | """ 24 | An optional start date of the timespan for the transaction history records you're requesting. The startDate must precede the endDate if you specify both dates. To be included in results, the transaction's purchaseDate must be equal to or greater than the startDate. 25 | 26 | https://developer.apple.com/documentation/appstoreserverapi/startdate 27 | """ 28 | 29 | endDate: Optional[int] = attr.ib(default=None) 30 | """ 31 | An optional end date of the timespan for the transaction history records you're requesting. Choose an endDate that's later than the startDate if you specify both dates. Using an endDate in the future is valid. To be included in results, the transaction's purchaseDate must be less than the endDate. 32 | 33 | https://developer.apple.com/documentation/appstoreserverapi/enddate 34 | """ 35 | 36 | productIds: Optional[List[str]] = attr.ib(default=None) 37 | """ 38 | An optional filter that indicates the product identifier to include in the transaction history. Your query may specify more than one productID. 39 | 40 | https://developer.apple.com/documentation/appstoreserverapi/productid 41 | """ 42 | 43 | productTypes: Optional[List[ProductType]] = attr.ib(default=None) 44 | """ 45 | An optional filter that indicates the product type to include in the transaction history. Your query may specify more than one productType. 46 | """ 47 | 48 | sort: Optional[Order] = attr.ib(default=None) 49 | """ 50 | An optional sort order for the transaction history records. The response sorts the transaction records by their recently modified date. The default value is ASCENDING, so you receive the oldest records first. 51 | """ 52 | 53 | subscriptionGroupIdentifiers: Optional[List[str]] = attr.ib(default=None) 54 | """ 55 | An optional filter that indicates the subscription group identifier to include in the transaction history. Your query may specify more than one subscriptionGroupIdentifier. 56 | 57 | https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier 58 | """ 59 | 60 | inAppOwnershipType: Optional[InAppOwnershipType] = attr.ib(default=None) 61 | """ 62 | An optional filter that limits the transaction history by the in-app ownership type. 63 | 64 | https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype 65 | """ 66 | 67 | revoked: Optional[bool] = attr.ib(default=None) 68 | """ 69 | An optional Boolean value that indicates whether the response includes only revoked transactions when the value is true, or contains only nonrevoked transactions when the value is false. By default, the request doesn't include this parameter. 70 | """ 71 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/ResponseBodyV2DecodedPayload.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | 4 | from attr import define 5 | import attr 6 | 7 | from .Data import Data 8 | from .ExternalPurchaseToken import ExternalPurchaseToken 9 | from .LibraryUtility import AttrsRawValueAware 10 | from .NotificationTypeV2 import NotificationTypeV2 11 | from .Subtype import Subtype 12 | from .Summary import Summary 13 | 14 | 15 | @define 16 | class ResponseBodyV2DecodedPayload(AttrsRawValueAware): 17 | """ 18 | A decoded payload containing the version 2 notification data. 19 | 20 | https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2decodedpayload 21 | """ 22 | 23 | notificationType: Optional[NotificationTypeV2] = NotificationTypeV2.create_main_attr('rawNotificationType') 24 | """ 25 | The in-app purchase event for which the App Store sends this version 2 notification. 26 | 27 | https://developer.apple.com/documentation/appstoreservernotifications/notificationtype 28 | """ 29 | 30 | rawNotificationType: Optional[str] = NotificationTypeV2.create_raw_attr('notificationType') 31 | """ 32 | See notificationType 33 | """ 34 | 35 | subtype: Optional[Subtype] = Subtype.create_main_attr('rawSubtype') 36 | """ 37 | Additional information that identifies the notification event. The subtype field is present only for specific version 2 notifications. 38 | 39 | https://developer.apple.com/documentation/appstoreservernotifications/subtype 40 | """ 41 | 42 | rawSubtype: Optional[str] = Subtype.create_raw_attr('subtype') 43 | """ 44 | See subtype 45 | """ 46 | 47 | notificationUUID: Optional[str] = attr.ib(default=None) 48 | """ 49 | A unique identifier for the notification. 50 | 51 | https://developer.apple.com/documentation/appstoreservernotifications/notificationuuid 52 | """ 53 | 54 | data: Optional[Data] = attr.ib(default=None) 55 | """ 56 | The object that contains the app metadata and signed renewal and transaction information. 57 | The data, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields. 58 | 59 | https://developer.apple.com/documentation/appstoreservernotifications/data 60 | """ 61 | 62 | version: Optional[str] = attr.ib(default=None) 63 | """ 64 | A string that indicates the notification's App Store Server Notifications version number. 65 | 66 | https://developer.apple.com/documentation/appstoreservernotifications/version 67 | """ 68 | 69 | signedDate: Optional[int] = attr.ib(default=None) 70 | """ 71 | The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data. 72 | 73 | https://developer.apple.com/documentation/appstoreserverapi/signeddate 74 | """ 75 | 76 | summary: Optional[Summary] = attr.ib(default=None) 77 | """ 78 | The summary data that appears when the App Store server completes your request to extend a subscription renewal date for eligible subscribers. 79 | The data, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields. 80 | 81 | https://developer.apple.com/documentation/appstoreservernotifications/summary 82 | """ 83 | 84 | externalPurchaseToken: Optional[ExternalPurchaseToken] = attr.ib(default=None) 85 | """ 86 | This field appears when the notificationType is EXTERNAL_PURCHASE_TOKEN. 87 | The data, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields. 88 | 89 | https://developer.apple.com/documentation/appstoreservernotifications/externalpurchasetoken 90 | """ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 1.9.0 4 | - Incorporate changes for App Store Server API v1.16 [https://github.com/apple/app-store-server-library-python/pull/141] from @riyazpanjwani 5 | - Fix SyntaxWarning in regex pattern string [https://github.com/apple/app-store-server-library-python/pull/138] from @krepe90 6 | 7 | ## Version 1.8.0 8 | - Incorporate changes for App Store Server API v1.15 and App Store Server Notifications v2.15 [https://github.com/apple/app-store-server-library-python/pull/134] 9 | 10 | ## Version 1.7.0 11 | - Update the SignedDataVerifier to cache verified certificate chains, improving performance [https://github.com/apple/app-store-server-library-python/pull/122] 12 | 13 | ## Version 1.6.0 14 | - Update README to improve Dependabot link discovery [https://github.com/apple/app-store-server-library-python/pull/119] 15 | 16 | ## Version 1.5.0 17 | - Add an async client built on httpx [https://github.com/apple/app-store-server-library-python/pull/105] 18 | - Drop Python 3.7 support [https://github.com/apple/app-store-server-library-python/pull/106] 19 | - Add check for original transaction id in legacy receipts [https://github.com/apple/app-store-server-library-python/pull/104] from @willhnation 20 | 21 | ## Version 1.4.0 22 | - Incorporate changes for App Store Server API v1.13 and App Store Server Notifications v2.13 [https://github.com/apple/app-store-server-library-python/pull/102] 23 | - Remove the upper limit on libraries that are unlikely to break upon upgrade [https://github.com/apple/app-store-server-library-python/pull/101] 24 | 25 | ## Version 1.3.0 26 | - Incorporate changes for App Store Server API v1.12 and App Store Server Notifications v2.12 [https://github.com/apple/app-store-server-library-python/pull/95] 27 | - Fix deprecation warnings from cryptography [https://github.com/apple/app-store-server-library-python/pull/94] from @WFT 28 | - Replace use of deprecated datetime.utcnow() [https://github.com/apple/app-store-server-library-python/pull/93] from @WFT 29 | - Cache cattrs values to prevent memory leak [https://github.com/apple/app-store-server-library-python/pull/92] from @Reskov 30 | 31 | ## Version 1.2.1 32 | - Fix issue with OfferType in JWSTransactionDecodedPayload [https://github.com/apple/app-store-server-library-python/pull/88] from @devinwang 33 | 34 | ## Version 1.2.0 35 | - Incorporate changes for App Store Server API v1.11 and App Store Server Notifications v2.11 [https://github.com/apple/app-store-server-library-python/pull/85] 36 | - Various documentation and quality of life improvements, including contributions from @CallumWatkins, @hakusai22, and @sunny-dubey 37 | 38 | ## Version 1.1.0 39 | - Support App Store Server Notifications v2.10 [https://github.com/apple/app-store-server-library-python/pull/65] 40 | - Bump cryptography and pyOpenSSL maximum versions [https://github.com/apple/app-store-server-library-python/pull/61]/[https://github.com/apple/app-store-server-library-python/pull/63] 41 | - Require appAppleId in SignedDataVerifier for the Production environment [https://github.com/apple/app-store-server-library-python/pull/60] 42 | 43 | ## 1.0.0 44 | - Add error message to APIException [https://github.com/apple/app-store-server-library-python/pull/52] 45 | 46 | ## 0.3.0 47 | - Add missing status field to the Data model [https://github.com/apple/app-store-server-library-python/pull/33] 48 | - Add error codes from App Store Server API v1.9 [https://github.com/apple/app-store-server-library-python/pull/39] 49 | - Add new fields from App Store Server API v1.10 [https://github.com/apple/app-store-server-library-python/pull/46] 50 | - Add support for reading unknown enum values [https://github.com/apple/app-store-server-library-python/pull/45] 51 | - Add support for Xcode and LocalTesting environments [https://github.com/apple/app-store-server-library-python/pull/44] 52 | 53 | ## 0.2.1 54 | - Add py.typed file [https://github.com/apple/app-store-server-library-python/pull/19] 55 | - Correct type annotation in PromotionalOfferSignatureCreator [https://github.com/apple/app-store-server-library-python/pull/17] 56 | 57 | ## 0.2.0 58 | 59 | - Correct type in LastTransactionsItem's status field [https://github.com/apple/app-store-server-library-python/pull/11] 60 | - Fix default value None for fields should require an Optional type [https://github.com/apple/app-store-server-library-python/pull/6] 61 | -------------------------------------------------------------------------------- /tests/test_payload_verification.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import unittest 4 | from base64 import b64decode 5 | from appstoreserverlibrary.models.Environment import Environment 6 | from appstoreserverlibrary.models.NotificationHistoryRequest import NotificationTypeV2 7 | 8 | from appstoreserverlibrary.signed_data_verifier import VerificationException, VerificationStatus, SignedDataVerifier 9 | 10 | from tests.util import get_signed_data_verifier, read_data_from_file 11 | 12 | class PayloadVerification(unittest.TestCase): 13 | def test_app_store_server_notification_decoding(self): 14 | verifier = get_signed_data_verifier(Environment.SANDBOX, "com.example") 15 | test_notification = read_data_from_file('tests/resources/mock_signed_data/testNotification') 16 | notification = verifier.verify_and_decode_notification(test_notification) 17 | self.assertEqual(notification.notificationType, NotificationTypeV2.TEST) 18 | 19 | def test_app_store_server_notification_decoding_production(self): 20 | verifier = get_signed_data_verifier(Environment.PRODUCTION, "com.example") 21 | test_notification = read_data_from_file('tests/resources/mock_signed_data/testNotification') 22 | with self.assertRaises(VerificationException) as context: 23 | verifier.verify_and_decode_notification(test_notification) 24 | self.assertEqual(context.exception.status, VerificationStatus.INVALID_ENVIRONMENT) 25 | 26 | def test_missing_x5c_header(self): 27 | verifier = get_signed_data_verifier(Environment.SANDBOX, "com.example") 28 | missing_x5c_header_claim = read_data_from_file('tests/resources/mock_signed_data/missingX5CHeaderClaim') 29 | with self.assertRaises(VerificationException) as context: 30 | verifier.verify_and_decode_notification(missing_x5c_header_claim) 31 | self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) 32 | 33 | def test_wrong_bundle_id_for_server_notification(self): 34 | verifier = get_signed_data_verifier(Environment.SANDBOX, "com.examplex") 35 | wrong_bundle = read_data_from_file('tests/resources/mock_signed_data/wrongBundleId') 36 | with self.assertRaises(VerificationException) as context: 37 | verifier.verify_and_decode_notification(wrong_bundle) 38 | self.assertEqual(context.exception.status, VerificationStatus.INVALID_APP_IDENTIFIER) 39 | 40 | def test_wrong_app_apple_id_for_server_notification(self): 41 | verifier = get_signed_data_verifier(Environment.PRODUCTION, "com.example", 1235) 42 | test_notification = read_data_from_file('tests/resources/mock_signed_data/testNotification') 43 | with self.assertRaises(VerificationException) as context: 44 | verifier.verify_and_decode_notification(test_notification) 45 | self.assertEqual(context.exception.status, VerificationStatus.INVALID_APP_IDENTIFIER) 46 | 47 | def test_renewal_info_decoding(self): 48 | verifier = get_signed_data_verifier(Environment.SANDBOX, "com.example") 49 | renewal_info = read_data_from_file('tests/resources/mock_signed_data/renewalInfo') 50 | notification = verifier.verify_and_decode_renewal_info(renewal_info) 51 | self.assertEqual(notification.environment, Environment.SANDBOX) 52 | 53 | def test_transaction_info_decoding(self): 54 | verifier = get_signed_data_verifier(Environment.SANDBOX, "com.example") 55 | transaction_info = read_data_from_file('tests/resources/mock_signed_data/transactionInfo') 56 | notification = verifier.verify_and_decode_signed_transaction(transaction_info) 57 | self.assertEqual(notification.environment, Environment.SANDBOX) 58 | 59 | def test_malformed_jwt_with_too_many_parts(self): 60 | verifier = get_signed_data_verifier(Environment.SANDBOX, "com.example") 61 | with self.assertRaises(VerificationException) as context: 62 | verifier.verify_and_decode_notification("a.b.c.d") 63 | self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) 64 | 65 | def test_malformed_jwt_with_malformed_data(self): 66 | verifier = get_signed_data_verifier(Environment.SANDBOX, "com.example") 67 | with self.assertRaises(VerificationException) as context: 68 | verifier.verify_and_decode_notification("a.b.c") 69 | self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) 70 | 71 | if __name__ == '__main__': 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/AppTransaction.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | 4 | from attr import define 5 | import attr 6 | 7 | from .LibraryUtility import AttrsRawValueAware 8 | 9 | from .Environment import Environment 10 | from .PurchasePlatform import PurchasePlatform 11 | 12 | @define 13 | class AppTransaction(AttrsRawValueAware): 14 | """ 15 | Information that represents the customer’s purchase of the app, cryptographically signed by the App Store. 16 | 17 | https://developer.apple.com/documentation/storekit/apptransaction 18 | """ 19 | 20 | receiptType: Optional[Environment] = Environment.create_main_attr('rawReceiptType') 21 | """ 22 | The server environment that signs the app transaction. 23 | 24 | https://developer.apple.com/documentation/storekit/apptransaction/3963901-environment 25 | """ 26 | 27 | rawReceiptType: Optional[str] = Environment.create_raw_attr('receiptType') 28 | """ 29 | See receiptType 30 | """ 31 | 32 | appAppleId: Optional[int] = attr.ib(default=None) 33 | """ 34 | The unique identifier the App Store uses to identify the app. 35 | 36 | https://developer.apple.com/documentation/storekit/apptransaction/3954436-appid 37 | """ 38 | 39 | bundleId: Optional[str] = attr.ib(default=None) 40 | """ 41 | The bundle identifier that the app transaction applies to. 42 | 43 | https://developer.apple.com/documentation/storekit/apptransaction/3954439-bundleid 44 | """ 45 | 46 | applicationVersion: Optional[str] = attr.ib(default=None) 47 | """ 48 | The app version that the app transaction applies to. 49 | 50 | https://developer.apple.com/documentation/storekit/apptransaction/3954437-appversion 51 | """ 52 | 53 | versionExternalIdentifier: Optional[int] = attr.ib(default=None) 54 | """ 55 | The version external identifier of the app 56 | 57 | https://developer.apple.com/documentation/storekit/apptransaction/3954438-appversionid 58 | """ 59 | 60 | receiptCreationDate: Optional[int] = attr.ib(default=None) 61 | """ 62 | The date that the App Store signed the JWS app transaction. 63 | 64 | https://developer.apple.com/documentation/storekit/apptransaction/3954449-signeddate 65 | """ 66 | 67 | originalPurchaseDate: Optional[int] = attr.ib(default=None) 68 | """ 69 | The date the user originally purchased the app from the App Store. 70 | 71 | https://developer.apple.com/documentation/storekit/apptransaction/3954448-originalpurchasedate 72 | """ 73 | 74 | originalApplicationVersion: Optional[str] = attr.ib(default=None) 75 | """ 76 | The app version that the user originally purchased from the App Store. 77 | 78 | https://developer.apple.com/documentation/storekit/apptransaction/3954447-originalappversion 79 | """ 80 | 81 | deviceVerification: Optional[str] = attr.ib(default=None) 82 | """ 83 | The Base64 device verification value to use to verify whether the app transaction belongs to the device. 84 | 85 | https://developer.apple.com/documentation/storekit/apptransaction/3954441-deviceverification 86 | """ 87 | 88 | deviceVerificationNonce: Optional[str] = attr.ib(default=None) 89 | """ 90 | The UUID used to compute the device verification value. 91 | 92 | https://developer.apple.com/documentation/storekit/apptransaction/3954442-deviceverificationnonce 93 | """ 94 | 95 | preorderDate: Optional[int] = attr.ib(default=None) 96 | """ 97 | The date the customer placed an order for the app before it's available in the App Store. 98 | 99 | https://developer.apple.com/documentation/storekit/apptransaction/4013175-preorderdate 100 | """ 101 | 102 | appTransactionId: Optional[str] = attr.ib(default=None) 103 | """ 104 | The unique identifier of the app download transaction. 105 | 106 | https://developer.apple.com/documentation/storekit/apptransaction/apptransactionid 107 | """ 108 | 109 | originalPlatform: Optional[PurchasePlatform] = PurchasePlatform.create_main_attr('rawOriginalPlatform') 110 | """ 111 | The platform on which the customer originally purchased the app. 112 | 113 | https://developer.apple.com/documentation/storekit/apptransaction/originalplatform-4mogz 114 | """ 115 | 116 | rawOriginalPlatform: Optional[str] = PurchasePlatform.create_raw_attr('originalPlatform') 117 | """ 118 | See originalPlatform 119 | """ -------------------------------------------------------------------------------- /appstoreserverlibrary/models/LibraryUtility.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from enum import EnumMeta 4 | from functools import lru_cache 5 | from typing import Any, List, Type, TypeVar 6 | from uuid import UUID 7 | 8 | from attr import Attribute, has, ib, fields 9 | from cattr import override 10 | from cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn, override 11 | import cattrs 12 | 13 | T = TypeVar('T') 14 | 15 | metadata_key = 'correspondingFieldName' 16 | metadata_type_key = 'typeOfField' 17 | 18 | class AppStoreServerLibraryEnumMeta(EnumMeta): 19 | def __contains__(c, val): 20 | try: 21 | c(val) 22 | except ValueError: 23 | return False 24 | return True 25 | 26 | def create_main_attr(c, raw_field_name: str, raw_required: bool = False) -> Any: 27 | def value_set(self, _: Attribute, value: c): 28 | newValue = value.value if value is not None else None 29 | if raw_required and newValue is None: 30 | raise ValueError(f"{raw_field_name} cannot be set to None when field is required") 31 | if newValue != getattr(self, raw_field_name): 32 | object.__setattr__(self, raw_field_name, newValue) 33 | return value 34 | return ib(default=None, on_setattr=value_set, metadata={metadata_key: raw_field_name, metadata_type_key: 'main'}) 35 | 36 | def create_raw_attr(c, field_name: str, required: bool = False) -> Any: 37 | def value_set(self, _: Attribute, value: str): 38 | if required and value is None: 39 | raise ValueError(f"raw{field_name[0].upper() + field_name[1:]} cannot be None") 40 | newValue = c(value) if value in c else None 41 | if newValue != getattr(self, field_name): 42 | object.__setattr__(self, field_name, newValue) 43 | return value 44 | 45 | def validate_not_none(instance, attribute, value): 46 | if value is None: 47 | raise ValueError(f"{attribute.name} cannot be None") 48 | 49 | if required: 50 | from attr import Factory 51 | def factory(instance): 52 | main_value = getattr(instance, field_name) 53 | if main_value is not None: 54 | return main_value.value 55 | raise ValueError(f"Either {field_name} or raw{field_name[0].upper() + field_name[1:]} must be provided") 56 | return ib(default=Factory(factory, takes_self=True), kw_only=True, on_setattr=value_set, validator=validate_not_none, metadata={metadata_key: field_name, metadata_type_key: 'raw'}) 57 | else: 58 | return ib(default=None, kw_only=True, on_setattr=value_set, metadata={metadata_key: field_name, metadata_type_key: 'raw'}) 59 | 60 | class AttrsRawValueAware: 61 | def __attrs_post_init__(self): 62 | attr_fields: List[Attribute] = fields(type(self)) 63 | for attribute in attr_fields: 64 | if metadata_type_key not in attribute.metadata or attribute.metadata[metadata_type_key] != 'raw': 65 | continue 66 | field: str = attribute.metadata.get(metadata_key) 67 | rawField = 'raw' + field[0].upper() + field[1:] 68 | rawValue = getattr(self, rawField) 69 | value = getattr(self, field) 70 | if rawValue is not None: 71 | setattr(self, rawField, rawValue) 72 | elif value is not None: 73 | setattr(self, field, value) 74 | 75 | 76 | @lru_cache(maxsize=None) 77 | def _get_cattrs_converter(destination_class: Type[T]) -> cattrs.Converter: 78 | c = cattrs.Converter() 79 | 80 | # Register UUID hooks to ensure lowercase serialization 81 | c.register_unstructure_hook(UUID, lambda uuid: str(uuid).lower() if uuid is not None else None) 82 | c.register_structure_hook(UUID, lambda d, _: UUID(d) if d is not None else None) 83 | 84 | # Need a function here because it must be a lambda based on cl, which is not always destination_class 85 | def make_overrides(cl): 86 | attributes: List[Attribute] = fields(cl) 87 | cattrs_overrides = {} 88 | # Use omit_if_default to prevent null fields from being serialized to JSON 89 | for attribute in attributes: 90 | if metadata_type_key in attribute.metadata: 91 | matching_name: str = attribute.metadata[metadata_key] 92 | if attribute.metadata[metadata_type_key] == 'raw': 93 | cattrs_overrides[matching_name] = override(omit=True) 94 | raw_field = 'raw' + matching_name[0].upper() + matching_name[1:] 95 | cattrs_overrides[raw_field] = override(rename=matching_name, omit_if_default=True) 96 | elif attribute.default is None and attribute.name not in cattrs_overrides: 97 | cattrs_overrides[attribute.name] = override(omit_if_default=True) 98 | return cattrs_overrides 99 | 100 | c.register_structure_hook_factory(has, lambda cl: make_dict_structure_fn(cl, c, **make_overrides(cl))) 101 | c.register_unstructure_hook_factory(has, lambda cl: make_dict_unstructure_fn(cl, c, **make_overrides(cl))) 102 | return c -------------------------------------------------------------------------------- /appstoreserverlibrary/receipt_utility.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | from base64 import b64decode 4 | from typing import Optional 5 | 6 | import asn1 7 | import base64 8 | import re 9 | 10 | PKCS7_OID = "1.2.840.113549.1.7.2" 11 | IN_APP_ARRAY = 17 12 | TRANSACTION_IDENTIFIER = 1703 13 | ORIGINAL_TRANSACTION_IDENTIFIER = 1705 14 | 15 | class ReceiptUtility: 16 | def extract_transaction_id_from_app_receipt(self, app_receipt: str) -> Optional[str]: 17 | """ 18 | Extracts a transaction id from an encoded App Receipt. Throws if the receipt does not match the expected format. 19 | *NO validation* is performed on the receipt, and any data returned should only be used to call the App Store Server API. 20 | 21 | :param appReceipt: The unmodified app receipt 22 | :return: A transaction id from the array of in-app purchases, null if the receipt contains no in-app purchases 23 | """ 24 | decoder = IndefiniteFormAwareDecoder() 25 | decoder.start(b64decode(app_receipt, validate=True)) 26 | tag = decoder.peek() 27 | if tag.typ != asn1.Types.Constructed or tag.nr != asn1.Numbers.Sequence: 28 | raise ValueError() 29 | decoder.enter() 30 | # PKCS#7 object 31 | tag, value = decoder.read() 32 | if tag.typ != asn1.Types.Primitive or tag.nr != asn1.Numbers.ObjectIdentifier or value != PKCS7_OID: 33 | raise ValueError() 34 | # This is the PKCS#7 format, work our way into the inner content 35 | decoder.enter() 36 | decoder.enter() 37 | decoder.read() 38 | decoder.read() 39 | decoder.enter() 40 | decoder.read() 41 | decoder.enter() 42 | tag, value = decoder.read() 43 | # Xcode uses nested OctetStrings, we extract the inner string in this case 44 | if tag.typ == asn1.Types.Constructed and tag.nr == asn1.Numbers.OctetString: 45 | inner_decoder = asn1.Decoder() 46 | inner_decoder.start(value) 47 | tag, value = inner_decoder.read() 48 | if tag.typ != asn1.Types.Primitive or tag.nr != asn1.Numbers.OctetString: 49 | raise ValueError() 50 | decoder = asn1.Decoder() 51 | decoder.start(value) 52 | tag = decoder.peek() 53 | if tag.typ != asn1.Types.Constructed or tag.nr != asn1.Numbers.Set: 54 | raise ValueError() 55 | decoder.enter() 56 | # We are in the top-level sequence, work our way to the array of in-apps 57 | while not decoder.eof(): 58 | decoder.enter() 59 | tag, value = decoder.read() 60 | if tag.typ == asn1.Types.Primitive and tag.nr == asn1.Numbers.Integer and value == IN_APP_ARRAY: 61 | decoder.read() 62 | tag, value = decoder.read() 63 | if tag.typ != asn1.Types.Primitive or tag.nr != asn1.Numbers.OctetString: 64 | raise ValueError() 65 | inapp_decoder = asn1.Decoder() 66 | inapp_decoder.start(value) 67 | inapp_decoder.enter() 68 | # In-app array 69 | while not inapp_decoder.eof(): 70 | inapp_decoder.enter() 71 | tag, value = inapp_decoder.read() 72 | if ( 73 | tag.typ == asn1.Types.Primitive 74 | and tag.nr == asn1.Numbers.Integer 75 | and (value == TRANSACTION_IDENTIFIER or value == ORIGINAL_TRANSACTION_IDENTIFIER) 76 | ): 77 | inapp_decoder.read() 78 | tag, value = inapp_decoder.read() 79 | singleton_decoder = asn1.Decoder() 80 | singleton_decoder.start(value) 81 | tag, value = singleton_decoder.read() 82 | return value 83 | inapp_decoder.leave() 84 | decoder.leave() 85 | return None 86 | 87 | def extract_transaction_id_from_transaction_receipt(self, transaction_receipt: str) -> Optional[str]: 88 | """ 89 | Extracts a transaction id from an encoded transactional receipt. Throws if the receipt does not match the expected format. 90 | *NO validation* is performed on the receipt, and any data returned should only be used to call the App Store Server API. 91 | :param transactionReceipt: The unmodified transactionReceipt 92 | :return: A transaction id, or null if no transactionId is found in the receipt 93 | """ 94 | decoded_top_level = base64.b64decode(transaction_receipt).decode('utf-8') 95 | matching_result = re.search(r'"purchase-info"\s+=\s+"([a-zA-Z0-9+/=]+)";', decoded_top_level) 96 | if matching_result: 97 | decoded_inner_level = base64.b64decode(matching_result.group(1)).decode('utf-8') 98 | inner_matching_result = re.search(r'"transaction-id"\s+=\s+"([a-zA-Z0-9+/=]+)";', decoded_inner_level) 99 | if inner_matching_result: 100 | return inner_matching_result.group(1) 101 | return None 102 | 103 | class IndefiniteFormAwareDecoder(asn1.Decoder): 104 | def _read_length(self) -> int: 105 | index, input_data = self.m_stack[-1] 106 | try: 107 | byte = input_data[index] 108 | except IndexError: 109 | raise asn1.Error('Premature end of input.') 110 | if byte == 0x80: 111 | # Xcode receipts use indefinite length encoding, not supported by all parsers 112 | # Indefinite length encoding is only entered, but never left during parsing for receipts 113 | # We therefore round up indefinite length encoding to be the remaining length 114 | self._read_byte() 115 | index, input_data = self.m_stack[-1] 116 | return len(input_data) - index 117 | return super()._read_length() -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [opensource-conduct@group.apple.com](mailto:opensource-conduct@group.apple.com). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /appstoreserverlibrary/models/ConsumptionRequest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import Optional 3 | 4 | from attr import define 5 | import attr 6 | 7 | from .AccountTenure import AccountTenure 8 | from .ConsumptionStatus import ConsumptionStatus 9 | from .DeliveryStatus import DeliveryStatus 10 | from .LibraryUtility import AttrsRawValueAware 11 | from .LifetimeDollarsPurchased import LifetimeDollarsPurchased 12 | from .LifetimeDollarsRefunded import LifetimeDollarsRefunded 13 | from .Platform import Platform 14 | from .PlayTime import PlayTime 15 | from .RefundPreference import RefundPreference 16 | from .UserStatus import UserStatus 17 | 18 | @define 19 | class ConsumptionRequest(AttrsRawValueAware): 20 | """ 21 | The request body containing consumption information. 22 | 23 | https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest 24 | """ 25 | 26 | customerConsented: Optional[bool] = attr.ib(default=None) 27 | """ 28 | A Boolean value that indicates whether the customer consented to provide consumption data to the App Store. 29 | 30 | https://developer.apple.com/documentation/appstoreserverapi/customerconsented 31 | """ 32 | 33 | consumptionStatus: Optional[ConsumptionStatus] = ConsumptionStatus.create_main_attr('rawConsumptionStatus') 34 | """ 35 | A value that indicates the extent to which the customer consumed the in-app purchase. 36 | 37 | https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus 38 | """ 39 | 40 | rawConsumptionStatus: Optional[int] = ConsumptionStatus.create_raw_attr('consumptionStatus') 41 | """ 42 | See consumptionStatus 43 | """ 44 | 45 | platform: Optional[Platform] = Platform.create_main_attr('rawPlatform') 46 | """ 47 | A value that indicates the platform on which the customer consumed the in-app purchase. 48 | 49 | https://developer.apple.com/documentation/appstoreserverapi/platform 50 | """ 51 | 52 | rawPlatform: Optional[int] = Platform.create_raw_attr('platform') 53 | """ 54 | See platform 55 | """ 56 | 57 | sampleContentProvided: Optional[bool] = attr.ib(default=None) 58 | """ 59 | A Boolean value that indicates whether you provided, prior to its purchase, a free sample or trial of the content, or information about its functionality. 60 | 61 | https://developer.apple.com/documentation/appstoreserverapi/samplecontentprovided 62 | """ 63 | 64 | deliveryStatus: Optional[DeliveryStatus] = DeliveryStatus.create_main_attr('rawDeliveryStatus') 65 | """ 66 | A value that indicates whether the app successfully delivered an in-app purchase that works properly. 67 | 68 | https://developer.apple.com/documentation/appstoreserverapi/deliverystatus 69 | """ 70 | 71 | rawDeliveryStatus: Optional[int] = DeliveryStatus.create_raw_attr('deliveryStatus') 72 | """ 73 | See deliveryStatus 74 | """ 75 | 76 | appAccountToken: Optional[str] = attr.ib(default=None) 77 | """ 78 | The UUID that an app optionally generates to map a customer's in-app purchase with its resulting App Store transaction. 79 | 80 | https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken 81 | """ 82 | 83 | accountTenure: Optional[AccountTenure] = AccountTenure.create_main_attr('rawAccountTenure') 84 | """ 85 | The age of the customer's account. 86 | 87 | https://developer.apple.com/documentation/appstoreserverapi/accounttenure 88 | """ 89 | 90 | rawAccountTenure: Optional[int] = AccountTenure.create_raw_attr('accountTenure') 91 | """ 92 | See accountTenure 93 | """ 94 | 95 | playTime: Optional[PlayTime] = PlayTime.create_main_attr('rawPlayTime') 96 | """ 97 | A value that indicates the amount of time that the customer used the app. 98 | 99 | https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest 100 | """ 101 | 102 | rawPlayTime: Optional[int] = PlayTime.create_raw_attr('playTime') 103 | """ 104 | See playTime 105 | """ 106 | 107 | lifetimeDollarsRefunded: Optional[LifetimeDollarsRefunded] = LifetimeDollarsRefunded.create_main_attr('rawLifetimeDollarsRefunded') 108 | """ 109 | A value that indicates the total amount, in USD, of refunds the customer has received, in your app, across all platforms. 110 | 111 | https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded 112 | """ 113 | 114 | rawLifetimeDollarsRefunded: Optional[int] = LifetimeDollarsRefunded.create_raw_attr('lifetimeDollarsRefunded') 115 | """ 116 | See lifetimeDollarsRefunded 117 | """ 118 | 119 | lifetimeDollarsPurchased: Optional[LifetimeDollarsPurchased] = LifetimeDollarsPurchased.create_main_attr('rawLifetimeDollarsPurchased') 120 | """ 121 | A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms. 122 | 123 | https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased 124 | """ 125 | 126 | rawLifetimeDollarsPurchased: Optional[int] = LifetimeDollarsPurchased.create_raw_attr('lifetimeDollarsPurchased') 127 | """ 128 | See lifetimeDollarsPurchased 129 | """ 130 | 131 | userStatus: Optional[UserStatus] = UserStatus.create_main_attr('rawUserStatus') 132 | """ 133 | The status of the customer's account. 134 | 135 | https://developer.apple.com/documentation/appstoreserverapi/userstatus 136 | """ 137 | 138 | rawUserStatus: Optional[int] = UserStatus.create_raw_attr('userStatus') 139 | """ 140 | See userStatus 141 | """ 142 | 143 | refundPreference: Optional[RefundPreference] = RefundPreference.create_main_attr('rawRefundPreference') 144 | """ 145 | A value that indicates your preference, based on your operational logic, as to whether Apple should grant the refund. 146 | 147 | https://developer.apple.com/documentation/appstoreserverapi/refundpreference 148 | """ 149 | 150 | rawRefundPreference: Optional[int] = RefundPreference.create_raw_attr('refundPreference') 151 | """ 152 | See refundPreference 153 | """ -------------------------------------------------------------------------------- /tests/test_xcode_signed_data.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | 3 | import unittest 4 | from appstoreserverlibrary.models.AutoRenewStatus import AutoRenewStatus 5 | from appstoreserverlibrary.models.Environment import Environment 6 | from appstoreserverlibrary.models.InAppOwnershipType import InAppOwnershipType 7 | from appstoreserverlibrary.models.OfferType import OfferType 8 | from appstoreserverlibrary.models.TransactionReason import TransactionReason 9 | from appstoreserverlibrary.models.Type import Type 10 | from appstoreserverlibrary.receipt_utility import ReceiptUtility 11 | from appstoreserverlibrary.signed_data_verifier import VerificationException 12 | 13 | from tests.util import get_signed_data_verifier, read_data_from_file 14 | 15 | XCODE_BUNDLE_ID = "com.example.naturelab.backyardbirds.example" 16 | 17 | class ReceiptUtilityTest(unittest.TestCase): 18 | def test_xcode_signed_app_transaction(self): 19 | verifier = get_signed_data_verifier(Environment.XCODE, XCODE_BUNDLE_ID) 20 | encoded_app_transaction = read_data_from_file("tests/resources/xcode/xcode-signed-app-transaction") 21 | 22 | app_transaction = verifier.verify_and_decode_app_transaction(encoded_app_transaction) 23 | 24 | self.assertIsNotNone(app_transaction) 25 | self.assertIsNone(app_transaction.appAppleId) 26 | self.assertEqual(XCODE_BUNDLE_ID, app_transaction.bundleId) 27 | self.assertEqual("1", app_transaction.applicationVersion) 28 | self.assertIsNone(app_transaction.versionExternalIdentifier) 29 | self.assertEqual(-62135769600000, app_transaction.originalPurchaseDate) 30 | self.assertEqual("1", app_transaction.originalApplicationVersion) 31 | self.assertEqual("cYUsXc53EbYc0pOeXG5d6/31LGHeVGf84sqSN0OrJi5u/j2H89WWKgS8N0hMsMlf", app_transaction.deviceVerification) 32 | self.assertEqual("48c8b92d-ce0d-4229-bedf-e61b4f9cfc92", app_transaction.deviceVerificationNonce) 33 | self.assertIsNone(app_transaction.preorderDate) 34 | self.assertEqual(Environment.XCODE, app_transaction.receiptType) 35 | self.assertEqual("Xcode", app_transaction.rawReceiptType) 36 | 37 | def test_xcode_signed_transaction(self): 38 | verifier = get_signed_data_verifier(Environment.XCODE, XCODE_BUNDLE_ID) 39 | encoded_transaction = read_data_from_file("tests/resources/xcode/xcode-signed-transaction") 40 | 41 | transaction = verifier.verify_and_decode_signed_transaction(encoded_transaction) 42 | 43 | self.assertEqual("0", transaction.originalTransactionId) 44 | self.assertEqual("0", transaction.transactionId) 45 | self.assertEqual("0", transaction.webOrderLineItemId) 46 | self.assertEqual(XCODE_BUNDLE_ID, transaction.bundleId) 47 | self.assertEqual("pass.premium", transaction.productId) 48 | self.assertEqual("6F3A93AB", transaction.subscriptionGroupIdentifier) 49 | self.assertEqual(1697679936049, transaction.purchaseDate) 50 | self.assertEqual(1697679936049, transaction.originalPurchaseDate) 51 | self.assertEqual(1700358336049, transaction.expiresDate) 52 | self.assertEqual(1, transaction.quantity) 53 | self.assertEqual(Type.AUTO_RENEWABLE_SUBSCRIPTION, transaction.type) 54 | self.assertEqual("Auto-Renewable Subscription", transaction.rawType) 55 | self.assertIsNone(transaction.appAccountToken) 56 | self.assertEqual(InAppOwnershipType.PURCHASED, transaction.inAppOwnershipType) 57 | self.assertEqual("PURCHASED", transaction.rawInAppOwnershipType) 58 | self.assertEqual(1697679936056, transaction.signedDate) 59 | self.assertIsNone(transaction.revocationReason) 60 | self.assertIsNone(transaction.revocationDate) 61 | self.assertFalse(transaction.isUpgraded) 62 | self.assertEqual(OfferType.INTRODUCTORY_OFFER, transaction.offerType) 63 | self.assertEqual(1, transaction.rawOfferType) 64 | self.assertIsNone(transaction.offerIdentifier) 65 | self.assertEqual(Environment.XCODE, transaction.environment) 66 | self.assertEqual("Xcode", transaction.rawEnvironment) 67 | self.assertEqual("USA", transaction.storefront) 68 | self.assertEqual("143441", transaction.storefrontId) 69 | self.assertEqual(TransactionReason.PURCHASE, transaction.transactionReason) 70 | self.assertEqual("PURCHASE", transaction.rawTransactionReason) 71 | 72 | def test_xcode_signed_renewal_info(self): 73 | verifier = get_signed_data_verifier(Environment.XCODE, XCODE_BUNDLE_ID) 74 | encoded_renewal_info = read_data_from_file("tests/resources/xcode/xcode-signed-renewal-info") 75 | 76 | renewal_info = verifier.verify_and_decode_renewal_info(encoded_renewal_info) 77 | 78 | self.assertIsNone(renewal_info.expirationIntent) 79 | self.assertEqual("0", renewal_info.originalTransactionId) 80 | self.assertEqual("pass.premium", renewal_info.autoRenewProductId) 81 | self.assertEqual("pass.premium", renewal_info.productId) 82 | self.assertEqual(AutoRenewStatus.ON, renewal_info.autoRenewStatus) 83 | self.assertEqual(1, renewal_info.rawAutoRenewStatus) 84 | self.assertIsNone(renewal_info.isInBillingRetryPeriod) 85 | self.assertIsNone(renewal_info.priceIncreaseStatus) 86 | self.assertIsNone(renewal_info.gracePeriodExpiresDate) 87 | self.assertIsNone(renewal_info.offerType) 88 | self.assertIsNone(renewal_info.offerIdentifier) 89 | self.assertEqual(1697679936711, renewal_info.signedDate) 90 | self.assertEqual(Environment.XCODE, renewal_info.environment) 91 | self.assertEqual("Xcode", renewal_info.rawEnvironment) 92 | self.assertEqual(1697679936049, renewal_info.recentSubscriptionStartDate) 93 | self.assertEqual(1700358336049, renewal_info.renewalDate) 94 | 95 | def test_xcode_signed_app_transaction_with_production_environment(self): 96 | verifier = get_signed_data_verifier(Environment.PRODUCTION, XCODE_BUNDLE_ID) 97 | encoded_app_transaction = read_data_from_file("tests/resources/xcode/xcode-signed-app-transaction") 98 | try: 99 | verifier.verify_and_decode_app_transaction(encoded_app_transaction) 100 | except VerificationException: 101 | return 102 | self.assertFalse(True) 103 | 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apple App Store Server Python Library 2 | The [Python](https://github.com/apple/app-store-server-library-python) server library for the [App Store Server API](https://developer.apple.com/documentation/appstoreserverapi), [App Store Server Notifications](https://developer.apple.com/documentation/appstoreservernotifications), and [Retention Messaging API](https://developer.apple.com/documentation/retentionmessaging). Also available in [Swift](https://github.com/apple/app-store-server-library-swift), [Node.js](https://github.com/apple/app-store-server-library-node), and [Java](https://github.com/apple/app-store-server-library-java). 3 | 4 | ## Table of Contents 5 | 1. [Installation](#installation) 6 | 2. [Documentation](#documentation) 7 | 3. [Usage](#usage) 8 | 4. [Support](#support) 9 | 10 | ## Installation 11 | 12 | #### Requirements 13 | 14 | - Python 3.8+ 15 | 16 | ### pip 17 | ```sh 18 | pip install app-store-server-library 19 | ``` 20 | 21 | ## Documentation 22 | 23 | [Documentation](https://apple.github.io/app-store-server-library-python/) 24 | 25 | [WWDC Video](https://developer.apple.com/videos/play/wwdc2023/10143/) 26 | 27 | ### Obtaining an In-App Purchase key from App Store Connect 28 | 29 | To 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. 30 | 31 | ### Obtaining Apple Root Certificates 32 | 33 | Download and store the root certificates found in the Apple Root Certificates section of the [Apple PKI](https://www.apple.com/certificateauthority/) site. Provide these certificates as an array to a SignedDataVerifier to allow verifying the signed data comes from Apple. 34 | 35 | ## Usage 36 | 37 | ### API Usage 38 | 39 | ```python 40 | from appstoreserverlibrary.api_client import AppStoreServerAPIClient, APIException 41 | from appstoreserverlibrary.models.Environment import Environment 42 | 43 | private_key = read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8") # Implementation will vary 44 | 45 | key_id = "ABCDEFGHIJ" 46 | issuer_id = "99b16628-15e4-4668-972b-eeff55eeff55" 47 | bundle_id = "com.example" 48 | environment = Environment.SANDBOX 49 | 50 | client = AppStoreServerAPIClient(private_key, key_id, issuer_id, bundle_id, environment) 51 | 52 | try: 53 | response = client.request_test_notification() 54 | print(response) 55 | except APIException as e: 56 | print(e) 57 | ``` 58 | 59 | ### Verification Usage 60 | 61 | ```python 62 | from appstoreserverlibrary.models.Environment import Environment 63 | from appstoreserverlibrary.signed_data_verifier import VerificationException, SignedDataVerifier 64 | 65 | root_certificates = load_root_certificates() 66 | enable_online_checks = True 67 | bundle_id = "com.example" 68 | environment = Environment.SANDBOX 69 | app_apple_id = None # appAppleId must be provided for the Production environment 70 | signed_data_verifier = SignedDataVerifier(root_certificates, enable_online_checks, environment, bundle_id, app_apple_id) 71 | 72 | try: 73 | signed_notification = "ey.." 74 | payload = signed_data_verifier.verify_and_decode_notification(signed_notification) 75 | print(payload) 76 | except VerificationException as e: 77 | print(e) 78 | ``` 79 | 80 | ### Receipt Usage 81 | 82 | ```python 83 | from appstoreserverlibrary.api_client import AppStoreServerAPIClient, APIException, GetTransactionHistoryVersion 84 | from appstoreserverlibrary.models.Environment import Environment 85 | from appstoreserverlibrary.receipt_utility import ReceiptUtility 86 | from appstoreserverlibrary.models.HistoryResponse import HistoryResponse 87 | from appstoreserverlibrary.models.TransactionHistoryRequest import TransactionHistoryRequest, ProductType, Order 88 | 89 | private_key = read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8") # Implementation will vary 90 | 91 | key_id = "ABCDEFGHIJ" 92 | issuer_id = "99b16628-15e4-4668-972b-eeff55eeff55" 93 | bundle_id = "com.example" 94 | environment = Environment.SANDBOX 95 | 96 | client = AppStoreServerAPIClient(private_key, key_id, issuer_id, bundle_id, environment) 97 | receipt_util = ReceiptUtility() 98 | app_receipt = "MI.." 99 | 100 | try: 101 | transaction_id = receipt_util.extract_transaction_id_from_app_receipt(app_receipt) 102 | if transaction_id != None: 103 | transactions = [] 104 | response: HistoryResponse = None 105 | request: TransactionHistoryRequest = TransactionHistoryRequest( 106 | sort=Order.ASCENDING, 107 | revoked=False, 108 | productTypes=[ProductType.AUTO_RENEWABLE] 109 | ) 110 | while response == None or response.hasMore: 111 | revision = response.revision if response != None else None 112 | response = client.get_transaction_history(transaction_id, revision, request, GetTransactionHistoryVersion.V2) 113 | for transaction in response.signedTransactions: 114 | transactions.append(transaction) 115 | print(transactions) 116 | except APIException as e: 117 | print(e) 118 | 119 | ``` 120 | 121 | ### Promotional Offer Signature Creation 122 | 123 | ```python 124 | from appstoreserverlibrary.promotional_offer import PromotionalOfferSignatureCreator 125 | import time 126 | 127 | private_key = read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8") # Implementation will vary 128 | 129 | key_id = "ABCDEFGHIJ" 130 | bundle_id = "com.example" 131 | 132 | promotion_code_signature_generator = PromotionalOfferSignatureCreator(private_key, key_id, bundle_id) 133 | 134 | product_id = "" 135 | subscription_offer_id = "" 136 | application_username = "" 137 | nonce = "" 138 | timestamp = round(time.time()*1000) 139 | base64_encoded_signature = promotion_code_signature_generator.create_signature(product_id, subscription_offer_id, application_username, nonce, timestamp) 140 | ``` 141 | 142 | ### Async HTTPX Support 143 | 144 | #### Pip 145 | Include the optional async dependency 146 | ```sh 147 | pip install app-store-server-library[async] 148 | ``` 149 | 150 | #### API Usage 151 | ```python 152 | from appstoreserverlibrary.api_client import AsyncAppStoreServerAPIClient, APIException 153 | from appstoreserverlibrary.models.Environment import Environment 154 | 155 | private_key = read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8") # Implementation will vary 156 | 157 | key_id = "ABCDEFGHIJ" 158 | issuer_id = "99b16628-15e4-4668-972b-eeff55eeff55" 159 | bundle_id = "com.example" 160 | environment = Environment.SANDBOX 161 | 162 | client = AsyncAppStoreServerAPIClient(private_key, key_id, issuer_id, bundle_id, environment) 163 | 164 | try: 165 | response = await client.request_test_notification() 166 | print(response) 167 | except APIException as e: 168 | print(e) 169 | 170 | # Once client use is finished 171 | await client.async_close() 172 | ``` 173 | 174 | ## Support 175 | 176 | Only the latest major version of the library will receive updates, including security updates. Therefore, it is recommended to update to new major versions. 177 | -------------------------------------------------------------------------------- /appstoreserverlibrary/jws_signature_creator.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | import datetime 4 | from typing import Any, Dict, Optional 5 | import base64 6 | import json 7 | import jwt 8 | import uuid 9 | 10 | from cryptography.hazmat.backends import default_backend 11 | from cryptography.hazmat.primitives import serialization 12 | 13 | from appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter 14 | 15 | class AdvancedCommerceAPIInAppRequest: 16 | def __init__(self): 17 | pass 18 | 19 | class JWSSignatureCreator: 20 | def __init__(self, audience: str, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str): 21 | self._audience = audience 22 | self._signing_key = serialization.load_pem_private_key(signing_key, password=None, backend=default_backend()) 23 | self._key_id = key_id 24 | self._issuer_id = issuer_id 25 | self._bundle_id = bundle_id 26 | 27 | def _create_signature(self, feature_specific_claims: Dict[str, Any]) -> str: 28 | claims = feature_specific_claims 29 | current_time = datetime.datetime.now(datetime.timezone.utc) 30 | 31 | claims["bid"] = self._bundle_id 32 | claims["iss"] = self._issuer_id 33 | claims["aud"] = self._audience 34 | claims["iat"] = current_time 35 | claims["nonce"] = str(uuid.uuid4()) 36 | 37 | return jwt.encode(claims, 38 | self._signing_key, 39 | algorithm="ES256", 40 | headers={"kid": self._key_id}, 41 | ) 42 | 43 | class PromotionalOfferV2SignatureCreator(JWSSignatureCreator): 44 | def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str): 45 | """ 46 | Create a PromotionalOfferV2SignatureCreator 47 | 48 | :param signing_key: Your private key downloaded from App Store Connect 49 | :param key_id: Your private key ID from App Store Connect 50 | :param issuer_id: Your issuer ID from the Keys page in App Store Connect 51 | :param bundle_id: Your app's bundle ID 52 | """ 53 | super().__init__(audience="promotional-offer", signing_key=signing_key, key_id=key_id, issuer_id=issuer_id, bundle_id=bundle_id) 54 | 55 | def create_signature(self, product_id: str, offer_identifier: str, transaction_id: Optional[str]) -> str: 56 | """ 57 | Create a promotional offer V2 signature. 58 | https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests 59 | 60 | :param product_id: The unique identifier of the product 61 | :param offer_identifier: The promotional offer identifier that you set up in App Store Connect 62 | :param transaction_id: The unique identifier of any transaction that belongs to the customer. You can use the customer's appTransactionId, even for customers who haven't made any In-App Purchases in your app. This field is optional, but recommended. 63 | :return: The signed JWS. 64 | """ 65 | if product_id is None: 66 | raise ValueError("product_id cannot be null") 67 | if offer_identifier is None: 68 | raise ValueError("offer_identifier cannot be null") 69 | feature_specific_claims = { 70 | "productId": product_id, 71 | "offerIdentifier": offer_identifier 72 | } 73 | if transaction_id is not None: 74 | feature_specific_claims["transactionId"] = transaction_id 75 | return self._create_signature(feature_specific_claims=feature_specific_claims) 76 | 77 | class IntroductoryOfferEligibilitySignatureCreator(JWSSignatureCreator): 78 | def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str): 79 | """ 80 | Create an IntroductoryOfferEligibilitySignatureCreator 81 | 82 | :param signing_key: Your private key downloaded from App Store Connect 83 | :param key_id: Your private key ID from App Store Connect 84 | :param issuer_id: Your issuer ID from the Keys page in App Store Connect 85 | :param bundle_id: Your app's bundle ID 86 | """ 87 | super().__init__(audience="introductory-offer-eligibility", signing_key=signing_key, key_id=key_id, issuer_id=issuer_id, bundle_id=bundle_id) 88 | 89 | def create_signature(self, product_id: str, allow_introductory_offer: bool, transaction_id: str) -> str: 90 | """ 91 | Create an introductory offer eligibility signature. 92 | https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests 93 | 94 | :param product_id: The unique identifier of the product 95 | :param allow_introductory_offer: A boolean value that determines whether the customer is eligible for an introductory offer 96 | :param transaction_id: The unique identifier of any transaction that belongs to the customer. You can use the customer's appTransactionId, even for customers who haven't made any In-App Purchases in your app. 97 | :return: The signed JWS. 98 | """ 99 | if product_id is None: 100 | raise ValueError("product_id cannot be null") 101 | if allow_introductory_offer is None: 102 | raise ValueError("allow_introductory_offer cannot be null") 103 | if transaction_id is None: 104 | raise ValueError("transaction_id cannot be null") 105 | feature_specific_claims = { 106 | "productId": product_id, 107 | "allowIntroductoryOffer": allow_introductory_offer, 108 | "transactionId": transaction_id 109 | } 110 | return self._create_signature(feature_specific_claims=feature_specific_claims) 111 | 112 | class AdvancedCommerceAPIInAppSignatureCreator(JWSSignatureCreator): 113 | def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str): 114 | """ 115 | Create an AdvancedCommerceAPIInAppSignatureCreator 116 | 117 | :param signing_key: Your private key downloaded from App Store Connect 118 | :param key_id: Your private key ID from App Store Connect 119 | :param issuer_id: Your issuer ID from the Keys page in App Store Connect 120 | :param bundle_id: Your app's bundle ID 121 | """ 122 | super().__init__(audience="advanced-commerce-api", signing_key=signing_key, key_id=key_id, issuer_id=issuer_id, bundle_id=bundle_id) 123 | 124 | def create_signature(self, advanced_commerce_in_app_request: AdvancedCommerceAPIInAppRequest) -> str: 125 | """ 126 | Create an Advanced Commerce in-app signed request. 127 | https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests 128 | 129 | :param advanced_commerce_in_app_request: The request to be signed. 130 | :return: The signed JWS. 131 | """ 132 | if advanced_commerce_in_app_request is None: 133 | raise ValueError("advanced_commerce_in_app_request cannot be null") 134 | c = _get_cattrs_converter(type(advanced_commerce_in_app_request)) 135 | request = c.unstructure(advanced_commerce_in_app_request) 136 | encoded_request = base64.b64encode(json.dumps(request).encode(encoding='utf-8')).decode('utf-8') 137 | feature_specific_claims = { 138 | "request": encoded_request 139 | } 140 | return self._create_signature(feature_specific_claims=feature_specific_claims) -------------------------------------------------------------------------------- /tests/test_jws_signature_creator.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Apple Inc. Licensed under MIT License. 2 | 3 | from attr import define 4 | import attr 5 | import base64 6 | import json 7 | import jwt 8 | import unittest 9 | from appstoreserverlibrary.jws_signature_creator import AdvancedCommerceAPIInAppRequest, AdvancedCommerceAPIInAppSignatureCreator, IntroductoryOfferEligibilitySignatureCreator, PromotionalOfferV2SignatureCreator 10 | from tests.util import read_data_from_binary_file 11 | 12 | @define 13 | class TestInAppRequest(AdvancedCommerceAPIInAppRequest): 14 | test_value: str 15 | 16 | class JWSSignatureCreatorTest(unittest.TestCase): 17 | def test_promotional_offer_signature_creator(self): 18 | signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') 19 | signature_creator = PromotionalOfferV2SignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') 20 | signature = signature_creator.create_signature("productId", "offerIdentifier", "transactionId") 21 | self.assertIsNotNone(signature) 22 | headers = jwt.get_unverified_header(signature) 23 | payload = jwt.decode(signature, options={"verify_signature": False}) 24 | 25 | # Header 26 | self.assertEqual("JWT", headers["typ"]) 27 | self.assertEqual("ES256", headers["alg"]) 28 | self.assertEqual("keyId", headers["kid"]) 29 | # Payload 30 | self.assertEqual("issuerId", payload['iss']) 31 | self.assertIsNotNone(payload['iat']) 32 | self.assertFalse('exp' in payload) 33 | self.assertEqual("promotional-offer", payload['aud']) 34 | self.assertEqual('bundleId', payload['bid']) 35 | self.assertIsNotNone(payload['nonce']) 36 | self.assertEqual('productId', payload['productId']) 37 | self.assertEqual('offerIdentifier', payload['offerIdentifier']) 38 | self.assertEqual('transactionId', payload['transactionId']) 39 | 40 | def test_promotional_offer_signature_creator_transaction_id_missing(self): 41 | signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') 42 | signature_creator = PromotionalOfferV2SignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') 43 | signature = signature_creator.create_signature("productId", "offerIdentifier", None) 44 | payload = jwt.decode(signature, options={"verify_signature": False}) 45 | self.assertFalse('transactionId' in payload) 46 | 47 | def test_promotional_offer_signature_creator_offer_identifier_missing(self): 48 | signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') 49 | signature_creator = PromotionalOfferV2SignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') 50 | with self.assertRaises(ValueError): 51 | signature_creator.create_signature("productId", None, "transactionId") 52 | 53 | def test_promotional_offer_signature_creator_product_id_missing(self): 54 | signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') 55 | signature_creator = PromotionalOfferV2SignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') 56 | with self.assertRaises(ValueError): 57 | signature_creator.create_signature(None, "offerIdentifier", "transactionId") 58 | 59 | def test_introductory_offer_eligibility_signature_creator(self): 60 | signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') 61 | signature_creator = IntroductoryOfferEligibilitySignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') 62 | signature = signature_creator.create_signature("productId", True, "transactionId") 63 | self.assertIsNotNone(signature) 64 | headers = jwt.get_unverified_header(signature) 65 | payload = jwt.decode(signature, options={"verify_signature": False}) 66 | 67 | # Header 68 | self.assertEqual("JWT", headers["typ"]) 69 | self.assertEqual("ES256", headers["alg"]) 70 | self.assertEqual("keyId", headers["kid"]) 71 | # Payload 72 | self.assertEqual("issuerId", payload['iss']) 73 | self.assertIsNotNone(payload['iat']) 74 | self.assertFalse('exp' in payload) 75 | self.assertEqual("introductory-offer-eligibility", payload['aud']) 76 | self.assertEqual('bundleId', payload['bid']) 77 | self.assertIsNotNone(payload['nonce']) 78 | self.assertEqual('productId', payload['productId']) 79 | self.assertEqual(True, payload['allowIntroductoryOffer']) 80 | self.assertEqual('transactionId', payload['transactionId']) 81 | 82 | def test_introductory_offer_eligibility_signature_creator_transaction_id_missing(self): 83 | signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') 84 | signature_creator = IntroductoryOfferEligibilitySignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') 85 | with self.assertRaises(ValueError): 86 | signature_creator.create_signature("productId", True, None) 87 | 88 | def test_introductory_offer_eligibility_signature_creator_allow_introductory_offer_missing(self): 89 | signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') 90 | signature_creator = IntroductoryOfferEligibilitySignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') 91 | with self.assertRaises(ValueError): 92 | signature_creator.create_signature("productId", None, "transactionId") 93 | 94 | def test_introductory_offer_eligibility_signature_creator_product_id_missing(self): 95 | signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') 96 | signature_creator = IntroductoryOfferEligibilitySignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') 97 | with self.assertRaises(ValueError): 98 | signature_creator.create_signature(None, True, "transactionId") 99 | 100 | def test_advanced_commerce_api_in_app_signature_creator(self): 101 | signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') 102 | signature_creator = AdvancedCommerceAPIInAppSignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') 103 | request = TestInAppRequest("testValue") 104 | signature = signature_creator.create_signature(request) 105 | self.assertIsNotNone(signature) 106 | headers = jwt.get_unverified_header(signature) 107 | payload = jwt.decode(signature, options={"verify_signature": False}) 108 | 109 | # Header 110 | self.assertEqual("JWT", headers["typ"]) 111 | self.assertEqual("ES256", headers["alg"]) 112 | self.assertEqual("keyId", headers["kid"]) 113 | # Payload 114 | self.assertEqual("issuerId", payload['iss']) 115 | self.assertIsNotNone(payload['iat']) 116 | self.assertFalse('exp' in payload) 117 | self.assertEqual("advanced-commerce-api", payload['aud']) 118 | self.assertEqual('bundleId', payload['bid']) 119 | self.assertIsNotNone(payload['nonce']) 120 | request = payload['request'] 121 | decode_json = json.loads(str(base64.b64decode(request).decode('utf-8'))) 122 | self.assertEqual('testValue', decode_json['test_value']) 123 | 124 | def test_advanced_commerce_api_in_app_signature_creator_request_missing(self): 125 | signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') 126 | signature_creator = AdvancedCommerceAPIInAppSignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') 127 | with self.assertRaises(ValueError): 128 | signature_creator.create_signature(None) -------------------------------------------------------------------------------- /appstoreserverlibrary/models/JWSRenewalInfoDecodedPayload.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Apple Inc. Licensed under MIT License. 2 | from typing import List, Optional 3 | 4 | from attr import define 5 | import attr 6 | from .AutoRenewStatus import AutoRenewStatus 7 | from .Environment import Environment 8 | 9 | from .ExpirationIntent import ExpirationIntent 10 | from .LibraryUtility import AttrsRawValueAware 11 | from .OfferType import OfferType 12 | from .PriceIncreaseStatus import PriceIncreaseStatus 13 | from .OfferDiscountType import OfferDiscountType 14 | 15 | @define 16 | class JWSRenewalInfoDecodedPayload(AttrsRawValueAware): 17 | """ 18 | A decoded payload containing subscription renewal information for an auto-renewable subscription. 19 | 20 | https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfodecodedpayload 21 | """ 22 | 23 | expirationIntent: Optional[ExpirationIntent] = ExpirationIntent.create_main_attr('rawExpirationIntent') 24 | """ 25 | The reason the subscription expired. 26 | 27 | https://developer.apple.com/documentation/appstoreserverapi/expirationintent 28 | """ 29 | 30 | rawExpirationIntent: Optional[int] = ExpirationIntent.create_raw_attr('expirationIntent') 31 | """ 32 | See expirationIntent 33 | """ 34 | 35 | originalTransactionId: Optional[str] = attr.ib(default=None) 36 | """ 37 | The original transaction identifier of a purchase. 38 | 39 | https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid 40 | """ 41 | 42 | autoRenewProductId: Optional[str] = attr.ib(default=None) 43 | """ 44 | The product identifier of the product that will renew at the next billing period. 45 | 46 | https://developer.apple.com/documentation/appstoreserverapi/autorenewproductid 47 | """ 48 | 49 | productId: Optional[str] = attr.ib(default=None) 50 | """ 51 | The unique identifier for the product, that you create in App Store Connect. 52 | 53 | https://developer.apple.com/documentation/appstoreserverapi/productid 54 | """ 55 | 56 | autoRenewStatus: Optional[AutoRenewStatus] = AutoRenewStatus.create_main_attr('rawAutoRenewStatus') 57 | """ 58 | The renewal status of the auto-renewable subscription. 59 | 60 | https://developer.apple.com/documentation/appstoreserverapi/autorenewstatus 61 | """ 62 | 63 | rawAutoRenewStatus: Optional[int] = AutoRenewStatus.create_raw_attr('autoRenewStatus') 64 | """ 65 | See autoRenewStatus 66 | """ 67 | 68 | isInBillingRetryPeriod: Optional[bool] = attr.ib(default=None) 69 | """ 70 | A Boolean value that indicates whether the App Store is attempting to automatically renew an expired subscription. 71 | 72 | https://developer.apple.com/documentation/appstoreserverapi/isinbillingretryperiod 73 | """ 74 | 75 | priceIncreaseStatus: Optional[PriceIncreaseStatus] = PriceIncreaseStatus.create_main_attr('rawPriceIncreaseStatus') 76 | """ 77 | The status that indicates whether the auto-renewable subscription is subject to a price increase. 78 | 79 | https://developer.apple.com/documentation/appstoreserverapi/priceincreasestatus 80 | """ 81 | 82 | rawPriceIncreaseStatus: Optional[int] = PriceIncreaseStatus.create_raw_attr('priceIncreaseStatus') 83 | """ 84 | See priceIncreaseStatus 85 | """ 86 | 87 | gracePeriodExpiresDate: Optional[int] = attr.ib(default=None) 88 | """ 89 | The time when the billing grace period for subscription renewals expires. 90 | 91 | https://developer.apple.com/documentation/appstoreserverapi/graceperiodexpiresdate 92 | """ 93 | 94 | offerType: Optional[OfferType] = OfferType.create_main_attr('rawOfferType') 95 | """ 96 | The type of subscription offer. 97 | 98 | https://developer.apple.com/documentation/appstoreserverapi/offertype 99 | """ 100 | 101 | rawOfferType: Optional[int] = OfferType.create_raw_attr('offerType') 102 | """ 103 | See offerType 104 | """ 105 | 106 | offerIdentifier: Optional[str] = attr.ib(default=None) 107 | """ 108 | The offer code or the promotional offer identifier. 109 | 110 | https://developer.apple.com/documentation/appstoreserverapi/offeridentifier 111 | """ 112 | 113 | signedDate: Optional[int] = attr.ib(default=None) 114 | """ 115 | The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data. 116 | 117 | https://developer.apple.com/documentation/appstoreserverapi/signeddate 118 | """ 119 | 120 | environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment') 121 | """ 122 | The server environment, either sandbox or production. 123 | 124 | https://developer.apple.com/documentation/appstoreserverapi/environment 125 | """ 126 | 127 | rawEnvironment: Optional[str] = Environment.create_raw_attr('environment') 128 | """ 129 | See environment 130 | """ 131 | 132 | recentSubscriptionStartDate: Optional[int] = attr.ib(default=None) 133 | """ 134 | The earliest start date of a subscription in a series of auto-renewable subscription purchases that ignores all lapses of paid service shorter than 60 days. 135 | 136 | https://developer.apple.com/documentation/appstoreserverapi/recentsubscriptionstartdate 137 | """ 138 | 139 | renewalDate: Optional[int] = attr.ib(default=None) 140 | """ 141 | The UNIX time, in milliseconds, that the most recent auto-renewable subscription purchase expires. 142 | 143 | https://developer.apple.com/documentation/appstoreserverapi/renewaldate 144 | """ 145 | 146 | currency: Optional[str] = attr.ib(default=None) 147 | """ 148 | The currency code for the renewalPrice of the subscription. 149 | 150 | https://developer.apple.com/documentation/appstoreserverapi/currency 151 | """ 152 | 153 | renewalPrice: Optional[int] = attr.ib(default=None) 154 | """ 155 | The renewal price, in milliunits, of the auto-renewable subscription that renews at the next billing period. 156 | 157 | https://developer.apple.com/documentation/appstoreserverapi/renewalprice 158 | """ 159 | 160 | offerDiscountType: Optional[OfferDiscountType] = OfferDiscountType.create_main_attr('rawOfferDiscountType') 161 | """ 162 | The payment mode you configure for the offer. 163 | 164 | https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype 165 | """ 166 | 167 | rawOfferDiscountType: Optional[str] = OfferDiscountType.create_raw_attr('offerDiscountType') 168 | """ 169 | See offerDiscountType 170 | """ 171 | 172 | eligibleWinBackOfferIds: Optional[List[str]] = attr.ib(default=None) 173 | """ 174 | An array of win-back offer identifiers that a customer is eligible to redeem, which sorts the identifiers to present the better offers first. 175 | 176 | https://developer.apple.com/documentation/appstoreserverapi/eligiblewinbackofferids 177 | """ 178 | 179 | appAccountToken: Optional[str] = attr.ib(default=None) 180 | """ 181 | The UUID that an app optionally generates to map a customer's in-app purchase with its resulting App Store transaction. 182 | 183 | https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken 184 | """ 185 | 186 | appTransactionId: Optional[str] = attr.ib(default=None) 187 | """ 188 | The unique identifier of the app download transaction. 189 | 190 | https://developer.apple.com/documentation/appstoreserverapi/appTransactionId 191 | """ 192 | 193 | offerPeriod: Optional[str] = attr.ib(default=None) 194 | """ 195 | The duration of the offer. 196 | 197 | https://developer.apple.com/documentation/appstoreserverapi/offerPeriod 198 | """ --------------------------------------------------------------------------------