← Alle Posts

Veröffentlicht 29. Mai 2026

Sandbox grün, Production rot: warum IAP nach dem Launch failed

Apple's IAP läuft durch vier Umgebungen, nicht zwei. Die fünf häufigsten Stellen wo's zwischen Sandbox und Production kaputtgeht, mit Fix dazu.

Kennt vermutlich jeder der schon mal eine App mit In-App-Purchases live gestellt hat: Review ist durch, du stellst auf „Ready for Sale”, und ein paar Stunden später kommt die erste Mail. Kauf geht nicht. Du machst Sandbox auf um’s nachzustellen. Läuft natürlich. Alles grün.

Das ist kein Zufall und auch nicht dein Pech. Sandbox und Production sind technisch zwei verschiedene Umgebungen (eigentlich vier, dazu gleich), und es gibt eine Handvoll Stellen wo’s genau dazwischen kaputtgeht. Die fünf häufigsten hab ich hier zusammengeschrieben, mit Fix dazu.

Erstmal: es sind vier Umgebungen, nicht zwei

Das ist der Kern vom ganzen Problem. Apple’s IAP läuft durch vier Umgebungen, und jede tickt anders:

  • StoreKit Configuration File: das lokale .storekit in Xcode. Offline, synthetische Receipts, kein echter Apple-Server im Spiel.
  • Sandbox: Apple’s Testserver. Eigene Sandbox-Apple-IDs, Subscriptions renewen im Schnelldurchlauf und maximal sechs Mal.
  • TestFlight: Production-Binary, aber Sandbox-Receipt. Seit Dezember 2024 renewen Subscriptions hier nur noch einmal alle 24 Stunden, egal wie lang die Subscription eigentlich ist.
  • Production: der echte Store.

Wenn du nur in einer davon testest (meistens dem .storekit-File, weil’s am bequemsten ist), hast du die anderen drei nie gesehen. Genau da kommen die Bugs her.

1. Status 21007

Der Klassiker bei allem was noch Apple’s altes verifyReceipt nutzt. Du schickst dein Receipt an einen von zwei Endpoints:

  • Production: https://buy.itunes.apple.com/verifyReceipt
  • Sandbox: https://sandbox.itunes.apple.com/verifyReceipt

Apple will: immer zuerst Production. Kommt Status 21007 zurück, dann Sandbox. Klingt simpel, der Haken ist nur, der HTTP-Status ist in beiden Fällen 200. Die 21007 steckt im JSON-Body. Wer auf den HTTP-Code prüft sieht sie nie und wundert sich warum nichts geht.

async function verifyReceipt(receiptB64) {
  const body = JSON.stringify({
    "receipt-data": receiptB64,
    password: process.env.IAP_SHARED_SECRET,
    "exclude-old-transactions": true,
  });
  let r = await fetch("https://buy.itunes.apple.com/verifyReceipt", { method: "POST", body });
  let j = await r.json();
  if (j.status === 21007) {
    r = await fetch("https://sandbox.itunes.apple.com/verifyReceipt", { method: "POST", body });
    j = await r.json();
  }
  return j;
}

Wichtig fürs Review: TestFlight-Builds haben Sandbox-Receipts, obwohl sie wie Production signiert sind. Und Apple-Reviewer testen mit TestFlight. Heißt: validierst du nur gegen Production, failed dein Review, und du checkst es nicht weil in Sandbox ja alles ging.

Wer neu baut nimmt eh besser gleich StoreKit 2 plus App Store Server API, da fällt der ganze 21007-Tanz weg. Aber für bestehende Apps mit verifyReceipt muss der Fallback rein, sonst geht’s irgendwann schief.

2. Das StoreKit-Config-File ist nicht Sandbox

Hängt direkt am vorigen Punkt. Das lokale .storekit-File ist super zum schnellen Iterieren, kein Login, kein Warten auf Renewals. Aber es ist eine eigene Welt mit eigenen Macken:

  • Die Receipts sind abgespeckt. Felder wie pending_renewal_info fehlen. Erwartet dein Server-Validator die, fliegt er auf die Nase.
  • Grace Periods gibt’s hier gar nicht, die siehst du frühestens in Production.
  • Transaction.all zeigt nicht immer alles an.

.storekit ist für Unit-Tests und Rumprobieren. Bevor du auf „submit” drückst, geh einmal komplett durch TestFlight mit einem echten Sandbox-Tester (eingeloggt unter Settings → Developer → Sandbox Apple Account, nicht unter Media & Purchases). Machst du das nicht, hast du Production schlicht nie getestet.

3. App Store Connect, wo am meisten schiefgeht

Produziert die meisten Tickets nach dem Launch, und das fiese ist: es failed lautlos. SKProductsRequest bzw. Product.products(for:) geben einfach ein leeres Array zurück, kein Error, nichts. Mögliche Gründe:

  • Paid Apps Agreement ist nicht „Active”. Banking und Steuerkram nicht fertig, keine Produkte. Häufigste Einzelursache überhaupt. Als DACH-Dev kommt noch das W-8BEN für die US-Quellensteuer dazu plus deine UID.
  • IAP hängt in „Missing Metadata”, z.B. weil der Review-Screenshot fehlt.
  • Bundle ID stimmt nicht überein zwischen Xcode und ASC. Case-sensitive, gern mal ein Tippfehler.
  • In-App-Purchase-Capability nicht in Xcode aktiviert.

Und der eine der jeden bei der ersten App erwischt: die allererste IAP muss zusammen mit dem ersten Binary eingereicht werden. Legst du die App ohne angehängte IAPs an und reichst die Käufe separat ein, kommt das hier zurück:

„We have returned your in-app purchase products to you as the required binary was not submitted…”

Steht bei Apple nur am Rande in der Doku. Fix: die IAPs explizit auf der Version-Seite der App anhängen, nicht nur in der Sidebar anlegen, und dem Reviewer kurz dazuschreiben dass die Käufe im Binary drin sind. Dann geht’s durch.

Noch was: nach dem Release dauert’s 24 bis 72 Stunden bis die IAPs überall durchpropagiert sind. Die 1-Stern-Reviews der ersten Tage sind oft genau das, nicht dein Code. Manuelles Release hilft dagegen.

4. Hardcodete Sandbox-URL im Release-Build

Der peinlichste, weil er so harmlos aussieht:

#if DEBUG
let url = "https://sandbox.itunes.apple.com/verifyReceipt"
#else
let url = "https://buy.itunes.apple.com/verifyReceipt"
#endif

Lokal funktioniert das einwandfrei. Dann geht’s ins Review und failed, weil TestFlight-Builds NICHT mit DEBUG kompiliert werden. Die laufen als Release, landen auf Production, kriegen 21007, und der Validator gibt auf.

Lösung ist dieselbe wie bei Punkt 1: die #if DEBUG-Weiche raus, immer Production zuerst, immer auf 21007 fallbacken. Apple’s Antwort entscheidet welchen Endpoint du nutzt, nicht dein Build-Setting.

5. Shared Secret / Bundle ID

Der nervigste, weil du ewig im Code suchst bevor du drauf kommst dass das Problem ein String in den Settings ist.

Zwei Shared Secrets gibt’s:

  • Primary (Master): ein Wert für den ganzen Account. ASC → Users and Access → Integrations → Shared Secret.
  • App-Specific: pro App. ASC → App → App Information → App-Specific Shared Secret. Sobald eine App das app-spezifische nutzt, geht das Master für die App nicht mehr. Steht so in Apple’s Doku.

Falsches Secret heißt: Receipt-Validierung schlägt fehl mit Status 21004, ohne brauchbare Fehlermeldung. Produkte fetched SKProductsRequest trotzdem normal, weil die Produkt-Abfrage kein Secret braucht. Der Fehler taucht erst beim Kauf-Validieren auf, also typischerweise wenn der erste echte User kauft.

Bundle-ID-Mismatch ist anders gelagert: da failed die Produkt-Abfrage selbst, weil die IAPs auf eine andere Bundle ID registriert sind. Du kriegst invalidProductIdentifiers zurück und gar keine Produkte angezeigt. Also einmal sauber gegenchecken, dass Bundle ID in Xcode und ASC exakt gleich sind.

Bei StoreKit 2 / App Store Server API gibt’s kein Shared Secret mehr, da brauchst du einen In-App-Purchase-Key (.p8 plus Issuer ID plus Key ID) und einen signierten JWT pro Request. Sauberer, aber zwei Dinge merken: der JWT darf max 60 Minuten gültig sein, und das .p8-File kannst du genau einmal runterladen. Weg ist weg, dann neuer Key.

Checkliste vor dem Submit

  • Paid Apps Agreement auf „Active” (Banking, Steuer, UID durch)
  • In-App-Purchase-Capability in Xcode an
  • Alle IAPs auf „Ready to Submit” mit Screenshot
  • Erste IAP am ersten Binary angehängt
  • Subscription Group angelegt (Pflicht bei Auto-Renewables)
  • Server: Production zuerst, Sandbox-Fallback bei 21007
  • Keine hardcodete Sandbox-URL im Release-Build
  • Server Notifications V2 mit Production- UND Sandbox-URL in ASC
  • Einmal komplett durch TestFlight mit Sandbox-Tester
  • Nach Release 24-72h Propagation einplanen

Wenn’s schon brennt

Falls du gerade Post-Launch-Tickets hast: drei Sachen zuerst. Kommen überhaupt Produkte zurück (sonst ASC)? Tauchen 21007 in den Server-Logs auf (sonst fehlt der Fallback)? Bundle ID und Secret korrekt?

Und wenn du grad feststeckst und keine Lust auf tagelanges Rumprobieren hast: sowas mach ich beruflich, auf Deutsch, für Indie-Devs im DACH-Raum. Schreib mir, dann schau ich’s mir an.

iapstorekitapp-store-connect21007subscription-debugging