What you need before starting
The performance test is sandbox-only. You need an active sandbox original transaction ID for the app and subscription product you want Apple to test. Do not use an app transaction ID, a production transaction ID, or a transaction from another bundle.
- A sandbox realtime URL configured through Apple's sandbox StoreKit API host.
- A published sandbox runtime snapshot with valid sandbox messages and offers.
- An active sandbox original transaction ID for an auto-renewable subscription.
- A runtime path that can return a valid response without reaching App Store Connect.
- Logs for latency, response shape, selected rule, fallback behavior, and request failures.
Timing note: Apple indicates performance-test execution and result availability can take time. In practice, plan for polling and allow up to 1 hour before treating a pending test as stuck.
Initiate the test and fetch the result
The performance-test endpoints are available on the sandbox StoreKit host. A passing result is required before configuring the production realtime URL.
POST https://api.storekit-sandbox.apple.com/inApps/v1/messaging/performanceTest
Authorization: Bearer {in_app_purchase_key_jwt}
Content-Type: application/json
{
"originalTransactionId": "2000000123456789"
}The initiate response returns a request identifier. Use that identifier to poll the result endpoint.
GET https://api.storekit-sandbox.apple.com/inApps/v1/messaging/performanceTest/result/{requestId}
Authorization: Bearer {in_app_purchase_key_jwt}Apple's configuration includes a response-time threshold. The sandbox documentation has referenced roughly 700ms, but your parser should read the threshold from the returned config instead of hardcoding assumptions.
How to read the result
The result response is not just pass or fail. It tells you how many requests are still pending, how often the runtime responded successfully, what the response times looked like, and which failures were counted.
| Field | Meaning | What to do |
|---|---|---|
result | Overall test status, such as PASS. | Production URL setup should wait for a passing result. |
numPending | Number of Apple test requests still not accounted for. | Keep polling until pending reaches 0 or the run clearly expires. |
successRate | Percentage of test requests with valid successful responses. | Investigate invalid response bodies, timeouts, and auth rejects. |
responseTimes | Latency distribution for your realtime endpoint. | Optimize cold starts, KV reads, signing, and rule evaluation. |
failures | Failure categories with counts. | Map each failure to a runtime log event before retrying. |
Sample result JSON
This example is illustrative, but it shows the shape you should make visible in your control plane. Operators need to see more than a pass badge.
{
"target": "https://runtime.retainkit.dev/apple/retention/app_9f21/sandbox",
"result": "PASS",
"numPending": 0,
"successRate": 100,
"config": {
"numRequests": 50,
"responseTimeThreshold": 700
},
"responseTimes": {
"min": 42,
"p50": 84,
"p95": 162,
"max": 231
},
"failures": {}
}
What PASS does not mean
Approve production messages. A sandbox PASS only proves your runtime can respond in time — it says nothing about production message review state.
Prove the production URL is set. A passing sandbox test is required before you can configure the production URL, but it is not the same as configuring it.
Validate every product and locale rule. The test uses one sandbox transaction. Rules covering other products, locales, or subscription groups need separate review.
A sandbox PASS is a gate, not a green light. Production readiness requires approved messages, configured production URL, and verified runtime behavior for each locale and product your rules cover.
Common errors and fixes
Apple's docs describe structured error types. In day-to-day setup, teams also run into numeric StoreKit-style errors. These two are especially common:
4000211 Invalid performance test request
4000006 Invalid transaction id- Invalid performance test request. Confirm the request body uses the expected transaction field and that the sandbox realtime URL is already configured.
- Invalid transaction id. Use the original transaction identifier from an active sandbox auto-renewable subscription, not a renewal transaction, app transaction ID, production ID, or family-sharing unsupported transaction.
- Existing test run. Wait for the current run to finish before starting another one.
- Timeouts. Remove App Store Connect reads from the request path, precompile config, and keep signing work bounded.
- Invalid responses. Ensure the response returns exactly one allowed branch:
message,promotionalOffer,alternateProduct, or the appropriate fallback behavior. - Environment mismatch. Sandbox tests must use sandbox messages, sandbox offers, sandbox keys, and the sandbox realtime URL.