Apple Retention Messaging API Taking a back-end integration from project intake to a sprint-ready, delivered backlog
A subscription studio is losing subscribers at the OS cancellation screen — and wants to intervene at that exact moment with a pre-approved save message, alternate plan, or offer.
The ask. Integrate Driftwell with Apple's Retention Messaging API so that, when a subscriber taps Cancel Subscription, the App Store calls our back-end in real time and renders the message it returns on the Confirm Cancellation screen.
The analyst's job. Turn a thin, partly pre-release capability into a shared understanding, a defensible contract, a de-risked plan, and a backlog engineering can build — surfacing what isn't known as deliberately as what is. The artefacts below are that work.
Getting to grips with the requirement
Before anyone writes code: establish an unambiguous, shared understanding of what the capability is, who participates, and where the work ends.
The capability lets an app show a pre-approved message — optionally with an image, an alternate plan, or a promotional offer — at the moment a subscriber attempts to cancel inside the OS. Two distinct planes of work fall out of that single sentence, and naming them early stops them being conflated downstream.
Configuration
Upload messages & images, get them approved per product + locale, hold the signing keys.
Real-time
Apple calls our endpoint during a live cancellation and expects a valid response inside a hard budget.
Actors & systems
| Actor / system | Role in the flow |
|---|---|
| Subscriber | Initiates cancellation on the OS subscription-management screen. |
| App Store | Detects the attempt, calls our endpoint, renders the returned message. |
| Tideline endpoint | Always-on back-end; verifies the signed request, selects a message, responds within budget. |
| Decision service | Eligibility evaluation, offer selection, promotional-offer signing. |
| App Store Connect | Config plane: upload, approval state, signing keys. |
| ASSN v2 | Async notifications used for saved-vs-cancelled outcome analytics. |
The four message types
Text
Title + subtitle only. Static, pre-approved.
Text + image
As above, plus an approved image asset.
Switch-plan
Message + a suggested alternate product chosen at runtime.
Promotional offer
Message + discounted continuation chosen at runtime.
The displayed text is static and pre-approved by Apple per locale — it cannot contain user-specific data. Only the selection of an offer or alternate product happens in real time. The endpoint isn't generating copy; it's choosing among pre-cleared options inside a latency budget.
End-to-end runtime flow
The live cancellation path, modelled as a sequence — including the configuration check, the latency budget, and the failure-to-default fallback.
Gap & clarification log
Discovery's real output isn't documenting what's known — it's surfacing what isn't. This is the artefact that protects delivery: every row is an open question with an impact and an owner.
| # | Gap / open question | Impact if left open | Risk | Owner |
|---|---|---|---|---|
| G1 | Exact field names & types in the request/response bodies (pre-release schema). | Build can't be finalised against an assumed contract; rework risk. | Critical | Analyst → Apple |
| G4 | Concrete eligibility rules (tenure, storefront, sample %) and precedence when several apply. | Determines who sees offers vs default; affects margin & fairness. | Critical | Product + Data |
| G5 | Offer abuse — can a user re-trigger cancel to farm discounts? Need a cooldown / cap. | Direct revenue leakage; reputational risk. | High | Analyst + Product |
| G6 | Agreed p99 latency budget within the 700 ms ceiling. | If unbudgeted, the perf test fails and prod won't enable. | Critical | Eng + Analyst |
| G8 | Signing keys — which .p8 signs offers; storage, rotation, blast radius. | A leaked key allows forged offers; need zero-downtime rotation. | Critical | Security + Eng |
| G9 | Inbound trust — verifying Apple's JWS (x5c to root, expiry, clock skew). | Without it the endpoint trusts unauthenticated callers. | Critical | Security |
| G12 | Privacy / GDPR — PII in the request, lawful basis, retention, log redaction. | UK/EU compliance; ICO exposure for a London company. | High | Legal + DPO |
| G14 | Environment parity — sandbox config does not carry to prod; who re-creates it, when? | Drift between envs; prod launches with untested messages. | High | Analyst + Eng |
| G7 | Fallback semantics — exact App Store behaviour on timeout vs 5xx vs malformed. | Mis-modelling the failure path degrades UX silently. | Confirm | Analyst → Apple |
Showing 9 of 15 logged gaps · full log carries G1–G15 across Contract, Decisioning, Security, Privacy & Schedule.
Functional & non-functional requirements
The gaps, once framed, resolve into testable requirements — and acceptance criteria written in BDD so business, dev and QA share one definition of done.
Functional — what it must do
Verify the inbound JWS against Apple's chain and reject anything unverifiable.
Select at most one outcome: a message, an alternate product, an offer, or "no message".
For an offer, generate a valid PromotionalOfferSignatureV2 in the response.
Reference only Apple-approved messages for the request's product + locale.
If ineligible, let the App Store render the configured default message.
Enforce a per-subscriber offer cooldown within a configurable window.
Non-functional — how well
Latency: within Apple's 700 ms ceiling; internal p99 ≤ 400 ms.
Availability ≥ 99.95%; degrade to default, never to an error screen.
Security: verify every request; keys in a secrets manager/HSM, rotated.
Maintainability: the decision matrix is config-driven — no hot-path redeploy.
Idempotency: a repeated request must not double-sign or double-count.
Privacy: PII minimised & redacted in logs; UK-GDPR basis confirmed.
Acceptance criteria · BDD / Gherkin
Feature: Real-time retention message selection Scenario: Eligible monthly subscriber is offered an annual discount Given an authenticated real-time request for product "driftwell.plus.monthly" And the storefront is "GBR" and the locale is "en-GB" And the subscriber's first purchase was more than 30 days ago And no retention offer has been served in the last 60 days When the endpoint selects a message Then the response references an Apple-approved promotional-offer message And it includes a valid PromotionalOfferSignatureV2 And the full response returns within the agreed internal budget Scenario: Decision service is slow Given the decision service has not responded within the latency budget When the deadline is reached Then the endpoint returns a safe response yielding Apple's default message And the timeout is recorded for monitoring
The endpoint contract
An OpenAPI spec for the endpoint we host and Apple calls. Authenticity comes from verifying Apple's payload signature — not a token we issue — so the security model is documented as a JWS scheme.
# Response — at most one outcome object is populated; # an empty body (or 204) defers to the default message. RealtimeResponseBody: type: object properties: messageIdentifier: # an Apple-approved message for product+locale type: string alternateProduct: # switch-plan outcome $ref: '#/components/schemas/AlternateProduct' promotionalOffer: # requires a V2 signature $ref: '#/components/schemas/PromotionalOffer' PromotionalOfferSignatureV2: # V2 wraps the signed claims in a single compact JWS, # signed with the subscription .p8 key over productId, # offerIdentifier, appAccountToken, nonce and timestamp. required: [jws] properties: jws: { type: string } # authorises the offer at redemption keyIdentifier:{ type: string } # for rotation / reference nonce: { type: string, format: uuid }
Field shapes follow Apple's documented behaviour and App Store Server API conventions, but exact attribute names are pre-release. That uncertainty is logged as G1 and owned by a time-boxed spike (DRFT-101) — the contract is honest about what still needs confirming.
Dependencies & critical path
The configuration plane is a dependency chain with real lead times — access approval, an Apple-run performance test, per-locale review. Mapping it is what stops these becoming late surprises.
Live App Store subscription
Hard prerequisite — no subscription, no API access.
Account Holder requests API access
Apple-gated, variable lead time — start early. Share the endpoint URL on the form.
Subscription .p8 key
Signs promotional offers. One-time download — re-issue if lost.
App Store Connect API key
Mints the JWT that authenticates upload calls.
Stand up sandbox endpoint + upload messages
Default message first (mandatory fallback). Images 3840×2160 PNG, no transparency. Review is per locale — sandbox approves instantly.
Apple performance test
GateNeeds an active sandbox/TestFlight transaction — .storekit files don't work. Can take ~1h; use the longest product so it doesn't expire mid-test. Production cannot be configured until this passes.
Re-create & approve messages in PRODUCTION
Sandbox config does not carry over — production messages are re-created and Apple-reviewed separately. Keep review on the critical path.
Wire App Store Server Notifications v2 → activate → live
Outcome analytics attribute saved-vs-cancelled. Activation flips messages live; deactivation is the de-facto kill switch.
Test strategy
The defining constraint: Apple is the caller and the OS renders the screen. Testing splits by who controls the boundary — what we own, vs the Apple-driven flow.
Apple is the caller, the JWS is signed by Apple's chain, the UI is drawn by the OS, and the performance test is an Apple-run gate. So a purely internal environment proves correctness, latency, resilience and security of what we build — but gives zero confidence on the Apple-driven end-to-end. Both are needed; neither substitutes for the other.
Environment coverage matrix
| Test type | stable-int | Apple sandbox | Production |
|---|---|---|---|
| Unit — decision / eligibility / cooldown | ✓ | – | – |
| Endpoint component (fixtures) | ✓ | – | – |
| JWS verification (+ negative) | ✓ | part | – |
| Contract / schema — G1 guard | ✓ | – | – |
| Latency / load (700 ms budget) | ✓ | – | – |
| Failure injection / fallback | ✓ | ✓ | – |
| Apple performance test (gate) | – | ✓ | – |
| E2E rendered UI per message type | – | ✓ | ✓ |
| Localisation per locale | – | ✓ | ✓ |
| Prod synthetic smoke | – | – | ✓ |
| Canary ramp + kill switch | – | – | ✓ |
| Coverage owner: Analyst + QA | logic & perf | the real flow | scale & rollout |
- Sandbox exit criteria — gate to production
- Apple performance test passed
- All four message types render on-device
- Fallback-to-default verified under forced failure
- All target locales approved & rendering
- Offer redemption & plan-switch confirmed E2E
- Latency within budget under load
- Observability & kill switch verified
Sprint-ready backlog
Analysis lands as work engineering can pick up: one Epic, sprint-sized stories, and requirements traceability so every issue has a documented reason to exist.
PromotionalOfferSignatureV2 + key managementShowing 10 of 16 issues · every story traces to an FR/NFR and, where it closes one, a gap-log entry.