The first rejection on Speech Route was the one that sent me into the wrong file for hours.

The paywall opened. No subscriptions appeared. Just the screen, with nothing to buy.

I was sure I'd implemented StoreKit wrong. The product request looked like a failed network call: you ask for products, you get an empty list back. I checked the identifiers. I checked the fetch. I checked the sandbox account. The code was doing what I'd asked it to.

This is Guideline 2.1, App Completeness. An app whose in-app purchases don't load looks unfinished, because to a reviewer it is unfinished. App Store Connect had the subscriptions on Missing Metadata. StoreKit had nothing to return.

The empty list is not an error

This is the part I wish someone had said out loud.

StoreKit does not throw when the paperwork is incomplete. It returns []. That is indistinguishable from a bug in your request, which is why the first hours always go into Xcode.

An empty list looks like a failed request. It is often a finished request with nothing to sell. The same [] comes back for at least five different unfinished things, and none of them has its own error message.

In my case the fetch was fine. The product IDs matched. The surrounding App Store Connect account was not done, and some of the app information sat on the client's side, unfilled. Until that is complete, you can create subscription products and they still will not come back to the binary.

The order I check things

I now treat this as an App Store Connect checklist, not a debugging session. App Store Connect first, device second, Xcode last. I run it before I touch the StoreKit code:

  1. The app record exists and uses the same bundle ID as Xcode.
  2. The Paid Applications Agreement is active, with tax and banking complete.
  3. Every subscription is off Missing Metadata, including a review screenshot.
  4. The product IDs match character for character.
  5. A sandbox tester is signed in on the device.

Only when all five are clear do I open the Swift.

App Store Connect has no app record

The Speech Route job started with an empty App Store Connect account. No app record. No products. The client wanted a subscription and a listing. There was nowhere to attach either.

StoreKit talks to products on an app record. If the record is missing, every paywall test is theatre. I did not understand that on day one. I understood it after 2.1 read an empty purchase screen as an incomplete app.

The record is the listing: name, bundle ID, privacy URL, screenshots, age rating. On Speech Route some of that information sat with the client. I could write StoreKit. I could not invent a privacy policy URL they had not hosted, and a file is not a URL. Filling the record is submission work, not leftover metadata.

Before the feature, I now want an app record with the same bundle ID as Xcode. Then products. Then the paywall. Hangar went through on the first try because I owned that record. Speech Route did not.

The Paid Applications Agreement lives on the developer account, not on the app record.

You can create subscription products, fill in a display name, pick a price, and still get nothing back in the binary until the agreement is signed and the tax and banking details attached to it are complete. A product that looks finished in App Store Connect is still invisible to StoreKit while that paperwork is waiting.

Signing the agreement is the obvious checkbox. Tax and banking are the part people skip, because they can stay "in progress" without a loud error anywhere. Apple will let you create the subscription. It will not return it to the app. There is no StoreKit error that says "the bank form is incomplete." You get the same empty array you would get from a wrong product ID or a missing sandbox sign-in.

On Speech Route, part of this sat on the client's side. I could write the paywall. I could not sign their banking form. Until that was done, every StoreKit log I read was telling me a story about code that was not the problem.

What I check, at the account level in Agreements, Tax, and Banking, not on the in-app purchases screen:

  • The Paid Applications Agreement shows as active.
  • Tax forms are filed.
  • Banking is attached.

If the agreement is not clear, I stop. Debugging the request is wasted time. And 2.1 does not care that the agreement was someone else's job.

Missing Metadata in App Store Connect

On Speech Route I could see subscription products in App Store Connect. They had names I recognised. They were not ready. The status was Missing Metadata.

Every auto-renewable subscription needs to leave that status before Apple will return it. Until the fields below are filled, the product exists in the UI and does not exist to the binary.

  • Display name
  • Description
  • Price for the storefront I am testing
  • Review screenshot
  • Availability — cleared for sale in that storefront
  • Paid Applications Agreement — tax and banking complete

A row in the In-App Purchases table is not a product StoreKit can sell. Missing Metadata is the status that says so. I open the product, not the list.

Display name and description are not optional flavour. They are the metadata the store uses to decide the product is real. A product you created in five minutes and meant to finish later is the product that wasted my first hours in Xcode.

On Speech Route, some of the missing metadata was information the client had not provided yet. I could not invent a localised display name and a review capture from an empty App Store Connect record. The paywall code was not going to fix that.

The review screenshot is the field I skipped

Display name and price feel like the product. The review screenshot is the field I skipped in my head.

It is not marketing art. It is the image of the purchase that Apple wants for review. Leave it blank and the subscription stays on Missing Metadata, which looks exactly like a StoreKit bug when you fetch it. There is no field in the StoreKit error that says "add a review screenshot."

If any of those are missing, I do not debug StoreKit. I finish the product.

Product ID does not match App Store Connect

On Speech Route the product identifiers matched and StoreKit still returned nothing. The other version of the same empty list is worse in a quieter way: the ID in the binary is close, not equal, and the request looks healthy.

A string that is almost the product ID will fetch successfully. You do not get a 404. You get [].

Where I look: App Store Connect, the product, the Product ID field. Then the constant or plist in the app. Character for character. The ones that waste a morning:

  • a prefix from an old bundle,
  • a missing dot,
  • a subscription I created twice.

Copy the ID from App Store Connect into the binary. Do not retype it from memory. If the strings match and the list is still empty, I stop blaming the ID and go back up this list.

Not signed in with a sandbox account

Purchases in development go through a sandbox tester you create in App Store Connect. Signed in with the Apple ID you use for the App Store as a customer, product requests fail closed. You get an empty list. It looks like the identifiers are wrong. It looks like the network failed.

The setup: Settings → App Store on the device or simulator, sign out of the personal account, run the app, and sign in with the sandbox tester when StoreKit asks.

On Speech Route I did check the sandbox account, and I still spent hours in the wrong file. The sandbox was not the cause of that first empty paywall. Checking it and seeing [] anyway is what sent me back into the request.

That is the trap. Sandbox only reveals products that exist. It does not create them. It does not sign the agreement. It does not fill in a review screenshot. So the check is not "did I remember sandbox?" The check is: sandbox tester signed in, then if the list is still empty, stop blaming the fetch.

What I do on a device now:

  • Create the sandbox tester before I write the paywall.
  • Treat "which account is on this device" as step one of the paywall test, not a footnote after the Swift is "done."
  • Sign out of the personal Apple ID on the hardware I will test, not only in the simulator.
  • Run the purchase sheet once, on a phone, because a reviewer is on a phone.

If products load in sandbox and the paywall still fails review, the problem has moved: dismissal, price, duration, links. That is Guideline 3.1.2, not StoreKit. If they never load, I go back to App Store Connect.

What the reviewer sees

Reviewers do not see your StoreKit logs. On Speech Route I had logs that said the request worked. Review had a screen with nothing on it. Guideline 2.1 is the screen, not the log.

They open the app. They open the paywall. They try to buy something. If nothing is there, the app is incomplete, and 2.1 is the guideline that covers incomplete apps. Review will never tell you which of the five App Store Connect problems it was.

I spent those hours in the wrong place because the empty list looks like a failed request. Now, when a paywall comes back empty, App Store Connect is the first place I look and Xcode is the last. The pre-submit list is that order written down.

Same rule outside Apple: Stripe and Polar

The work on this site has included payment and revenue setups with Stripe and Polar. The lesson matched Speech Route: the account is the product.

StoreKit returned nothing while the Paid Applications Agreement waited. Card charging has the same shape: keys, webhooks, and a business that is allowed to take money. The first thing I check is whether this account can take a payment in the mode I am testing. If it cannot, the handler is not the bug, and I look at the account before I debug the webhook.

I do not have a specific Stripe or Polar failure written up yet, so there is no snippet here. When I do, I will write it the way I wrote a blank env var on Vercel: error text, then the fix.

The rest of that first submission

The rest of Speech Route's first submission failed for different reasons. The paywall you couldn't close is Guideline 3.1.2. The privacy policy that was a file instead of a URL is 5.1.1. Once products load, the prices on the paywall should come from StoreKit too, which is why I stopped hardcoding $9.99. The overview of the whole run is in the eight-rejections post.