---
title: "Why does RevenueDot answer 4xx or 5xx to a receipt?"
description: "A 4xx tells the SDK the purchase is permanently bad, so it finishes the transaction for good. A 5xx means try again later, so the SDK keeps the transaction and retries."
url: https://revenuedot.app/docs/help/receipt-errors-4xx-vs-5xx
---

# Why does RevenueDot answer 4xx or 5xx to a receipt?

The HTTP status tells the SDK what to do with the store transaction. A **4xx** means the purchase can never be accepted, so the SDK finishes (or, on Android, stops retrying) the transaction for good. A **5xx** means RevenueDot or the store had a temporary problem, so the SDK keeps the transaction open and posts it again later. RevenueDot never answers 4xx for its own failures, so a server bug cannot make a paying customer lose a purchase.

The SDK side of this rule is in the SDKs' own source. In purchases-ios, a failed post is "finishable" only when the status is not a server error ([NetworkError.swift](https://github.com/RevenueCat/purchases-ios/blob/main/Sources/Networking/HTTPClient/NetworkError.swift)). purchases-android makes the same split with `isServerError` ([Backend.kt](https://github.com/RevenueCat/purchases-android/blob/main/purchases/src/main/kotlin/com/revenuecat/purchases/common/Backend.kt)).

## What every error looks like
Every error has a JSON body with a numeric `code` and a `message`:

```bash
curl -s -w '\n%{http_code}\n' http://localhost:8787/v1/receipts \
  -H "Authorization: Bearer $TEST_KEY" -H "Content-Type: application/json" \
  -d '{"app_user_id":"user_1","fetch_token":"not-a-token","product_id":"pro_monthly"}'
```
```text
{"code":7103,"message":"The receipt is not a valid Test Store purchase token."}
400
```

## The codes, and where each one comes from
The codes are defined in [`apps/server/src/errors.ts`](https://github.com/revenuedot/revenuedot/blob/main/apps/server/src/errors.ts). They are the numbers the RevenueCat SDKs already understand.

### Permanent: 4xx, the SDK finishes the transaction
| HTTP | Code | Meaning | Thrown in | When |
|---|---|---|---|---|
| 401 | 7225 | Invalid API key | `routes/sdk.ts` | The `Authorization: Bearer` key is missing or unknown. Every SDK call answers this, not only receipts |
| 400 | 7220 | Invalid app user ID | `routes/sdk.ts` | `app_user_id` is empty or longer than 100 characters |
| 400 | 7000 | Bad request | `routes/sdk.ts` | A receipt posted with a secret key (`sk_`) has no `X-Platform` header, so RevenueDot cannot tell which app it is for |
| 400 | 7662 | Receipts for this store are not supported yet | `routes/sdk.ts` | The key belongs to an Amazon, Stripe, Web Billing, Paddle or Roku app |
| 400 | 7103 | Invalid receipt | `routes/sdk.ts` | The body has neither `fetch_token` nor `app_transaction` |
| 400 | 7103 | Invalid receipt | `stores/test-store.ts` | The Test Store token is not `test_<ms>_<id>`, or `product_id` is missing |
| 400 | 7103 | Invalid receipt | `stores/apple/index.ts` | The StoreKit 2 transaction's signature does not verify, the bundle ID does not match the app, the app receipt cannot be parsed, or it is an Xcode receipt without the `xcode_certificate` credential |
| 400 | 7103 | Invalid receipt | `stores/google/index.ts`, `stores/google/api.ts` | The purchase token is missing or Google says it is not valid, `product_ids` is missing for a one-time product, or a pending purchase was cancelled |
| 400 | 7102 | Receipt already in use | `services/purchases.ts` | Another known user owns this purchase and the project's transfer behaviour is `keep`, or `transfer_if_no_active` while that user still has an active subscription |

### Temporary: 5xx, the SDK retries
| HTTP | Code | Meaning | Thrown in | When |
|---|---|---|---|---|
| 500 | 7234 | App Store in-app purchase key problem | `stores/apple/index.ts` | A StoreKit 1 receipt arrived and the app has no in-app purchase key. The SDK retries after you add the key |
| 500 | 7234 | App Store in-app purchase key problem | `stores/apple/api.ts` | The key is incomplete, the private key is not a valid `.p8`, or Apple rejected it with 401 |
| 503 | 7101 | Store problem | `stores/apple/api.ts`, `stores/apple/index.ts` | Apple could not be reached, answered 429 or 5xx, or returned something RevenueDot could not verify |
| 503 | 7101 | Store problem | `stores/google/index.ts`, `stores/google/api.ts` | The Google purchase is still pending payment, the service account is missing or rejected, the package name is wrong, or Google failed |
| 500 | 7110 | Internal error | `errors.ts` | Anything unexpected inside RevenueDot, such as a database error |

A wrong App Store key or Google service account is your setup problem, not the customer's. That is why it answers 5xx: the purchase stays on the device until you fix the credentials.

## What to do
1. **Read the `message`.** It names the cause, for example the bundle ID it expected.
2. **For 7225,** check that the SDK uses the public key of an app in this project. See [Which key goes where](https://revenuedot.app/docs/concepts/projects-and-apps.md#which-key-goes-where).
3. **For 7103 from the App Store,** check the app's `bundle_id` matches the build you are testing. For Xcode StoreKit testing, add the `xcode_certificate` credential. See [How do I test purchases without real money?](https://revenuedot.app/docs/help/test-sandbox-purchases.md)
4. **For 7103 from Google,** check the app's `package_name`, and that the purchase was made by this app.
5. **For 7102,** decide who should own restored purchases and set `transfer_behavior`. See [How do I restore purchases?](https://revenuedot.app/docs/help/restore-purchases.md)
6. **For 7662,** use an App Store, Mac App Store, Google Play or Test Store app. Other stores are not supported yet.
7. **For 7234,** add the App Store in-app purchase key to the app. See [Connect the App Store](https://revenuedot.app/docs/guides/app-store.md). For local development only, you can set `allow_unsigned_receipts`.
8. **For 7101 from Google,** run **Verify credentials** on the app page, or `POST /v2/projects/{project_id}/apps/{app_id}/actions/verify_credentials`, and grant the service account access in Play Console. See [Connect Google Play](https://revenuedot.app/docs/guides/google-play.md).
9. **For 7110,** read the server log. The error is printed there with its stack. Open an issue if it looks like a bug.

After you fix a 5xx cause, you do not need to change the app. The transaction is still unfinished on the device, so the SDK posts it again the next time it retries pending transactions.

## Related
- [Why is my entitlement not active?](https://revenuedot.app/docs/help/entitlement-not-active.md)
- [Troubleshooting by symptom](https://revenuedot.app/docs/help/troubleshooting.md)
- [API errors](https://revenuedot.app/docs/api/errors.md)
- [SDK endpoints](https://revenuedot.app/docs/api/sdk-endpoints.md)
