Case Study Technical Analysis · Intake → Delivery

Apple Retention Messaging API Taking a back-end integration from project intake to a sprint-ready, delivered backlog

RoleTechnical Analyst
ClientTideline Labs (worked example)
DomainiOS subscriptions · StoreKit
SpanDiscovery → Delivery
Output7 analysis artefacts

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.

The path from intake to delivery
01
Discovery
Brief & flow
02
Gap log
Open questions
03
Requirements
FR / NFR · BDD
04
Contract
OpenAPI
05
Dependencies
Critical path
06
Test strategy
Coverage
07
Development start
Jira backlog

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.

Plane A

Configuration

Upload messages & images, get them approved per product + locale, hold the signing keys.

Done ahead of time · async
Plane B

Real-time

Apple calls our endpoint during a live cancellation and expects a valid response inside a hard budget.

On the hot path · < 700 ms

Actors & systems

Actor / systemRole in the flow
SubscriberInitiates cancellation on the OS subscription-management screen.
App StoreDetects the attempt, calls our endpoint, renders the returned message.
Tideline endpointAlways-on back-end; verifies the signed request, selects a message, responds within budget.
Decision serviceEligibility evaluation, offer selection, promotional-offer signing.
App Store ConnectConfig plane: upload, approval state, signing keys.
ASSN v2Async notifications used for saved-vs-cancelled outcome analytics.

The four message types

Text

Title + subtitle only. Static, pre-approved.

StaticNo signing

Text + image

As above, plus an approved image asset.

StaticNo signing

Switch-plan

Message + a suggested alternate product chosen at runtime.

Dynamic selectionNo signing

Promotional offer

Message + discounted continuation chosen at runtime.

Dynamic selectionSigned JWS
The nuance that shapes everything

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.

Subscriber
User
App Store
Apple server
Tideline EP
Our endpoint
Decision svc
Internal logic
Taps “Cancel Subscription”
Default message configured for product + locale?
alt
no default configured
Standard cancellation screen — no message
else  default configured
POST signed real-time requestJWS · x5c chain
⏱ hard budget < 700 ms — network + verify + decide + sign + serialise
Verify Apple JWS
Decode claims · evaluate eligibility
Select message / alt product / offer
opt
promotional offer selected
Generate PromotionalOfferSignatureV2
Decision
RealtimeResponseBodymessage id / offer / 204 none
alt
responded in time & valid
Confirm Cancellation — with message
else  timeout / 5xx / malformed
Pre-approved DEFAULT message (fallback)
Don’t cancel / redeem / switch / confirm
App Store Server Notification → analyticsasync · ASSN v2
fig. 1 — Get Retention Message · real-time plane with default-message fallback
#Gap / open questionImpact if left openRiskOwner
G1Exact field names & types in the request/response bodies (pre-release schema).Build can't be finalised against an assumed contract; rework risk.CriticalAnalyst → Apple
G4Concrete eligibility rules (tenure, storefront, sample %) and precedence when several apply.Determines who sees offers vs default; affects margin & fairness.CriticalProduct + Data
G5Offer abuse — can a user re-trigger cancel to farm discounts? Need a cooldown / cap.Direct revenue leakage; reputational risk.HighAnalyst + Product
G6Agreed p99 latency budget within the 700 ms ceiling.If unbudgeted, the perf test fails and prod won't enable.CriticalEng + Analyst
G8Signing keys — which .p8 signs offers; storage, rotation, blast radius.A leaked key allows forged offers; need zero-downtime rotation.CriticalSecurity + Eng
G9Inbound trust — verifying Apple's JWS (x5c to root, expiry, clock skew).Without it the endpoint trusts unauthenticated callers.CriticalSecurity
G12Privacy / GDPR — PII in the request, lawful basis, retention, log redaction.UK/EU compliance; ICO exposure for a London company.HighLegal + DPO
G14Environment parity — sandbox config does not carry to prod; who re-creates it, when?Drift between envs; prod launches with untested messages.HighAnalyst + Eng
G7Fallback semantics — exact App Store behaviour on timeout vs 5xx vs malformed.Mis-modelling the failure path degrades UX silently.ConfirmAnalyst → Apple

Showing 9 of 15 logged gaps · full log carries G1–G15 across Contract, Decisioning, Security, Privacy & Schedule.

Functional — what it must do

FR-2

Verify the inbound JWS against Apple's chain and reject anything unverifiable.

FR-4

Select at most one outcome: a message, an alternate product, an offer, or "no message".

FR-5

For an offer, generate a valid PromotionalOfferSignatureV2 in the response.

FR-6

Reference only Apple-approved messages for the request's product + locale.

FR-7

If ineligible, let the App Store render the configured default message.

FR-8

Enforce a per-subscriber offer cooldown within a configurable window.

Non-functional — how well

NFR-1

Latency: within Apple's 700 ms ceiling; internal p99 ≤ 400 ms.

NFR-2

Availability ≥ 99.95%; degrade to default, never to an error screen.

NFR-4

Security: verify every request; keys in a secrets manager/HSM, rotated.

NFR-8

Maintainability: the decision matrix is config-driven — no hot-path redeploy.

NFR-9

Idempotency: a repeated request must not double-sign or double-count.

NFR-5

Privacy: PII minimised & redacted in logs; UK-GDPR basis confirmed.

Acceptance criteria · BDD / Gherkin

real_time_selection.featureGherkin
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
POST /v1/retention-message retention.tidelinelabs.com
Apple sends a signed real-time request. The server verifies the signature, evaluates eligibility, and returns at most one message outcome. On any internal failure it returns 204 so Apple renders the pre-approved default — the failure path is a first-class design decision, not an afterthought.
200
Message outcome selected. Body carries a message id, alternate product, or signed promotional offer.
204
No override. Ineligible subscriber or internal timeout — App Store shows the configured default.
400
Malformed request body.
401
Inbound JWS could not be verified against Apple's certificate chain.
retention-message.openapi.yamlOpenAPI 3.1
# 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 }
Reconciliation flagged, not assumed

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.

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.

parallel

Subscription .p8 key

Signs promotional offers. One-time download — re-issue if lost.

parallel

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

Gate

Needs 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.

What cannot be mocked away

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 typestable-intApple sandboxProduction
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 + QAlogic & perfthe real flowscale & 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
DRFT-100
Integrate Driftwell with Apple Retention Messaging APIEpic · 1 spike · 15 stories
16
issues
71
points
DRFT-101Spike3
Reconcile Apple real-time request / response schema
AC: OpenAPI updated to match the live reference · sample sandbox payload captured as a test fixture · deltas from the draft contract circulated to the team. Time-boxed.
G1contractretention-messagingspike
DRFT-102Story5
Stand up Get Retention Message endpoint (sandbox URL)
↳ 101FR-1
DRFT-103Story5
Verify inbound Apple JWS signature
↳ 102FR-2NFR-4G9
DRFT-105Story8
Config-driven eligibility rule engine
↳ 104FR-4NFR-8G4
DRFT-107Story5
Per-subscriber offer cooldown / abuse guard
↳ 106FR-8G5
DRFT-108Story8
Generate PromotionalOfferSignatureV2 + key management
AC: signature validates against Apple in sandbox · signing latency stays within the hot-path budget · key sourced only from the secrets store · rotation runbook exists and is rehearsed.
↳ 106FR-5NFR-4G8securitysigning
DRFT-109Story5
Response serialisation, 204 default-deferral & timeout path
↳ 106FR-6FR-7NFR-1
DRFT-113Story5
Performance / load harness — prove the 700 ms budget
↳ 102NFR-1NFR-3G6
DRFT-115Story3
Production cutover + prod message re-creation runbook
↳ 113NFR-10G14
DRFT-116Story3
Canary via random-sample eligibility + kill switch
↳ 109, 115NFR-2

Showing 10 of 16 issues · every story traces to an FR/NFR and, where it closes one, a gap-log entry.