Use the StoreKit/In-App Purchase key

promotionalOfferSignatureV2 is not signed with a generic App Store Connect API key. Use the StoreKit/In-App Purchase key material from App Store Connect. Operationally, that means your runtime needs the issuer ID, key ID, private .p8 key, bundle ID, product ID, promotional offer identifier, nonce, timestamp, and the transaction identifier Apple sent in the realtime request.

Keep that private key server-side. Do not expose it to the browser, the mobile app, a build script checked into source, or a client-side analytics tool.

Transaction binding: Apple notes that the signature requires a transaction ID. For Retention Messaging, use the originalTransactionId from the decoded realtime request body.

JWS payload fields

The V2 signature is a compact JWS. The exact signing helper can come from Apple's App Store Server Library or your own ES256 signer, but the payload should carry the promotional-offer claims Apple expects.

{ "iss": "57246542-96fe-1a63-e053-0824d011072a", "iat": 1778912345, "aud": "promotional-offer", "bid": "com.example.app", "nonce": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "productId": "premium.yearly", "offerIdentifier": "SAVE50", "transactionId": "2000000123456789" }

The important gotcha: productId and offerIdentifier are inside the JWS payload. They are not top-level fields in the realtime response JSON. The top-level response contains only the promotionalOffer object with a message identifier and the signed JWS.

Full realtime response example

A promotional-offer response references an approved Retention Messaging message. Apple requires the message to be approved and suitable for promotional-offer display. Do not attach an image to a promotional-offer message.

{ "promotionalOffer": { "messageIdentifier": "d6db64ea-f018-4ab1-8852-0b33d7c5eeb1", "promotionalOfferSignatureV2": "eyJhbGciOiJFUzI1NiIsImtpZCI6Ik1YVDdKOUQyIn0.eyJpc3MiOiI1NzI0NjU0Mi05NmZlLTFhNjMtZTA1My0wODI0ZDAxMTA3MmEiLCJhdWQiOiJwcm9tb3Rpb25hbC1vZmZlciJ9.MEUCIQD..." } }

The runtime should log the selected rule, message identifier, offer identifier, and signing result. Hash or encrypt transaction identifiers in logs.

Runtime signing pattern

Offer selection and offer signing are different responsibilities. Selection should be deterministic from a published runtime snapshot. Signing should happen only after the selected rule is known and the decoded Apple request has been validated.

  • Validate the incoming signed payload and confirm the app identifier.
  • Match product, storefront, locale, environment, and transaction state against the published retention flow.
  • Confirm the selected message is approved and the selected promotional offer exists for the product.
  • Generate a nonce and JWS with the StoreKit/In-App Purchase key.
  • Return the promotionalOffer response immediately.
  • If signing fails, return a safe approved fallback message or no dynamic offer rather than sending a malformed offer.

Common mistakes

  • Wrong key type. Use the StoreKit/In-App Purchase key for promotional-offer signing, not an unrelated App Store Connect API key.
  • Wrong transaction ID. Use originalTransactionId from Apple's decoded realtime request body.
  • Wrong response shape. Do not put productId or offerIdentifier beside promotionalOfferSignatureV2 in response JSON.
  • Pending message. A promotional-offer response can fail if the referenced Retention Messaging message is still PENDING.
  • Image on offer message. Promotional-offer messages should not include an image.
  • Signing in the browser. Signing belongs in the server runtime only.

Sources

Upload messagesConfigure the realtime URLAPI guide