App store connect API returns 401 with production url but works with sandbox url

Hi,

My app is currently in review (wasn't approved yet).

I'm using the Get Transaction History API with a sandbox user transaction ID. I successfully get results when using the sandbox url (https://api.storekit-sandbox.itunes.apple.com/inApps/v1/history/%7BoriginalTransactionId%7D)

However, when using the production url (https://api.storekit.itunes.apple.com/inApps/v1/history/%7BoriginalTransactionId%7D) - I'm consistently getting 401 - unauthorized, which according to the doc means something is wrong with my JWT.

Should I generate my JWT differently for sandbox vs production? If not, what else could cause this issue?

Thanks,

Replies

You're correct that a 401 indicates an issue with your JWT. A properly-constructed JWT should work for both the sandbox and production environments. Make sure to read through these articles carefully: https://developer.apple.com/documentation/appstoreserverapi/creating_api_keys_to_use_with_the_app_store_server_api https://developer.apple.com/documentation/appstoreserverapi/generating_tokens_for_api_requests

Pay extra attention to the "bid" field in the payload, which should be your app's bundle ID. Keep in mind that app bundle IDs are case-sensitive. If you're still having issues, I recommend filing a Feedback Ticket and posting the FB number here. Include as much info as you can, such as the JWT you're using and the code used to construct it, and we can help investigate further.

http://feedbackassistant.apple.com

Note: this is regarding the App Store Server API

Thanks for your response,

For anyone else who encounters the issue - after releasing the app to the app store, I no longer get 401 response, and the API works as expected for both production and sandbox TIDs.

Hi, i am still not able to get the response successully

here is my code in node js

    const now = Math.round(new Date().getTime() / 1000);
    const expirationTime = now + 1999;

    const privateKey = fs.readFileSync('./AuthKey_W467528DJL.p8')
    const headers = {
        algorithm: "ES256",
        header: {
            alg: "ES256",
            kid: "W467528DJL",
            typ: "JWT",
        },
    };

    let payload = {
        "iss": "8b3af5d2-f293-452c-8df1-6d023149279b",
        "iat": now,
        "exp": expirationTime,
        "aud": "appstoreconnect-v1",
        "bid": "org.momly.app"
    }

    const token = jwt.sign(payload, privateKey, headers);

    const callHeaders = {
        "Accept": "application/a-gzip; application/json",
        "Authorization": "Bearer " + token,
    }

    axios.get('https://api.appstoreconnect.apple.com/v1/apps', { headers: callHeaders }).then((response) => {
        console.log("Response", response.toJSON())
    }).catch((error) => {
        console.log("error", error.message)
    })
};

Please let me know what is the exact issue.

i have tried everything thats there over the internet

  • Error in "jwt.sign()": secretOrPrivateKey must be an asymmetric key when using ES256

  • I realized that Apple gave me one symmetrical key. How do I convert it to asymmetric?

Add a Comment

Not sure if this matters any longer to @aprvm @kostasoft, but for anyone finding this thread in the future, major issue with this sample code seems to be the calculation of expirationTime. Maximum time allowed for tokens (I recall seeing some different values in the documentation) is 20 minutes (1200 sec). So your increment of 1999 is too large and causes the token to be rejected. When I tried using an increment of 900 (15 minutes), everything worked (using my app details) with the change in headers below.

I also used different headers - const callHeaders = {"Authorization": "Bearer " + token, "Content-type": "application/json"}. Your "Accept" header containing "application/a-gzip" did not work for me (406 error). Removing that value and using either "Accept" or "Content-type" key in the header worked with "application/json" as the value.

Hey yall!, My issue seemed to be a matter of 2 things:

  1. Wrong headers, I was using algorithm instead of alg.
  2. Like @davessabrad mentioned, the date was too high, so a lowered expiration date worked (15 mins)

I suggest carefully reading the Apple docs and seeing how they define things! It's unfortunate because I did not find an example coming from them.

Happy Coding!


const jwt = require('jsonwebtoken');
const fs = require('fs');

const now = Math.round(new Date().getTime() / 1000);
const expirationTime = now + 900; // Set to 15 minutes (900 seconds)

exports.genrateAppStoreJwt = async () => {
  // Your credentials from App Store Connect
    const keyId = process.env.KEY_ID;
    const issuerId = process.env.ISSUER_ID;
    const privateKey = fs.readFileSync('YOUR_PATH');
    const bundleId = process.env.BUNDLE_ID;

    // Create the JWT header and payload

    const header = {
      'alg': 'ES256',
      'kid': keyId,
      'typ': 'JWT'
    };

    const payload = {
      "iss": issuerId,
      "iat": now,
      "exp": expirationTime,
      "aud": 'appstoreconnect-v1',
      "bid": bundleId,
    };

    console.log('payload: ', payload);

    // Generate the JWT
    const token = jwt.sign(payload, privateKey, { header: header, algorithm: 'ES256' });

    console.log(`Generated JWT: ${token}`);
    return token;
}

It would be really nice if they would update the documentation on this. I've been fiddling with JWT tokens for 5 hours. Shouldn't it return 4040010 instead of 401 in this case?