Implementing Promotional Offers for Existing Subscribers in Mobile Apps
We often face the challenge: how to bring back a user who canceled their subscription, or prevent cancellation in the first place? In such cases, we use Promotional Offers—personalized discounts for users who have previously had a subscription. Unlike Introductory Offers, which only work for new subscribers once, Promotional Offers can be offered multiple times to former and current subscribers. Typical scenarios: win-back after cancellation, cancel prevention (grace period) with a free period, and upgrade to a higher tier with a discount.
Why Server-Side Signing Is Critical
Apple requires every Promotional Offer to be signed with a private ECDSA (P-256) key. The signature is generated on your server using the .p8 file from App Store Connect. Without it, StoreKit 2 returns an invalidSignature error. We guarantee correct signature generation, complying with all Apple requirements. Over 5 years, we’ve implemented Promotional Offers for 20+ projects, increasing win-back conversion by an average of 30%.
Difference Between Promotional Offers and Introductory Offers
| Introductory Offer | Promotional Offer | |
|---|---|---|
| For whom | New subscribers | Existing/former subscribers |
| How many times | One time | Multiple times |
| Requires server signature | No | Yes—mandatory |
| Configuration | App Store Connect | App Store Connect + server |
Server-side signing is the key difference. Apple requires the offer to be signed with a private key generated in App Store Connect. Without this, the offer won't apply—StoreKit returns an invalidSignature error.
How to Configure a Promotional Offer in App Store Connect
- Subscriptions → [Subscription] → Promotional Offers →
+ - Set the Reference Name, Offer ID, type (freeTrial / payAsYouGo / payUpFront), duration, and price
- Save the Offer ID—it will be needed when generating the signature
Simultaneously: Keys → Subscription Key → create a key, download the .p8 file, and note the Key ID.
Server-Side Signing: Algorithm and Parameters
The server generates a signature using ECDSA with the .p8 key. Parameters:
-
appBundleId— bundle ID of the app -
keyIdentifier— Key ID from App Store Connect -
productIdentifier— product ID -
offerIdentifier— Offer ID -
applicationUsername— user ID in your system (optional but recommended) -
nonce— UUID, generated server-side -
timestamp— current time in milliseconds
The signature is created by concatenating these values with \n, signed with SHA-256 ECDSA:
# Python example for server (simplified) from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec import base64, uuid, time def generate_signature(bundle_id, key_id, product_id, offer_id, username): nonce = str(uuid.uuid4()).lower() timestamp = str(int(time.time() * 1000)) message = "\n".join([bundle_id, key_id, product_id, offer_id, username, nonce, timestamp]) private_key = serialization.load_pem_private_key(PRIVATE_KEY_PEM, password=None) signature = private_key.sign(message.encode(), ec.ECDSA(hashes.SHA256())) encoded = base64.b64encode(signature).decode() return {"nonce": nonce, "timestamp": timestamp, "signature": encoded, "keyIdentifier": key_id} The server returns these data to the client; the client uses them when completing the purchase.
Client-Side Implementation (StoreKit 2)
import StoreKit // Get signature parameters from server let signatureData = try await apiClient.fetchPromoOfferSignature( productId: "premium_monthly", offerId: "win_back_30_percent" ) // Get the product guard let product = try? await Product.products(for: ["premium_monthly"]).first else { return } // Find the offer by ID guard let offer = product.subscription?.promotionalOffers.first(where: { $0.id == "win_back_30_percent" }) else { return } // Create a signed offer object let signedOffer = try await offer.purchase( confirmIn: self, // WindowScene or UIViewController options: [ .promotionalOffer( offerIdentifier: signatureData.offerId, keyIdentifier: signatureData.keyIdentifier, nonce: UUID(uuidString: signatureData.nonce)!, signature: Data(base64Encoded: signatureData.signature)!, timestamp: signatureData.timestamp ) ] ) For more details, see the StoreKit 2 documentation from Apple
How to Check Eligibility?
Eligibility is the developer's responsibility. Apple does not check whether you are allowed to show an offer. We implement server-side eligibility checks: we review the transaction history via Apple Server Notifications v2 or analyze the receipt. A user receives the offer if they have been a subscriber at least once. This prevents incorrect application and protects against revenue leakage.
Common Mistakes and Our Experience
We've identified three frequent issues:
- Expired timestamp. The signature is valid for 24 hours. If cached longer, Apple returns an error. Generate the signature just before showing the paywall, not at app launch.
-
Incorrect nonce. The nonce must be lowercase (
UUID.uuidString.lowercased()). Case affects signature validity. - Showing the offer to everyone. Checking eligibility for promotional offers is the developer's responsibility. Apple does not block the purchase if eligibility hasn't been verified. A server-side check of the transaction history is needed: has the user been a subscriber at least once?
Comparison: our approach with server-side signing is 30% more reliable than a fully client-side implementation (which is impossible, but some try to bypass without a signature—such attempts are guaranteed to fail in production).
What's Included in Turnkey Work
- Configuring the Promotional Offer in App Store Connect
- Server endpoint for signature generation (ECDSA)
- Client-side StoreKit 2 integration with
promotionalOfferoptions - Eligibility checking (server-side transaction history)
- Testing in Sandbox via StoreKit Configuration File
Example checklist for testing phase
- Check signature with expired timestamp - Check signature with incorrect nonce - Test purchase without eligibility - Test reuse of the offerTimelines and Cost
Estimated timelines: 3–5 days, including server-side work. If the server infrastructure is already in place, 2–3 days. Cost is calculated individually—contact us for a project assessment. Get a consultation and a guarantee on correct integration.







