Problem Generating Signature for Subscription Offers – Error Code 18

I'm successfully using Apple subscriptions in my app, but I'm encountering SKErrorCodeDomain error 18 when trying to apply a subscription offer.

I want apply offer code first time only for subscription. Below are details of what i set in appstore and what i have tested.

Subscription Offer Details

  • Offer Type: For the first month
  • Customer Eligibility: New, Existing, and Expired Subscribers
  • Code Status: Active

Offer Code Creation Steps:

  • App Store Connect → App → Subscription → Select Subscription Product → Offer Codes → Add → Add Custom Codes

Signature Generation for Promotional Offers

I'm following Apple's documentation to generate a signature:
https://developer.apple.com/documentation/storekit/generating-a-signature-for-promotional-offers

I’ve constructed the payload as instructed:

appBundleId + '\u2063' + keyIdentifier + '\u2063' + productIdentifier + '\u2063' + offerIdentifier + '\u2063' + appAccountToken + '\u2063' + nonce + '\u2063' + timestamp

Keys and Identifiers

  • keyIdentifier, issuerId, and .p8 file are obtained from:
    • App Store Connect → Users and Access → Integrations → In-App Purchase
  • Test user created under:
    • App Store Connect → Users and Access → Sandbox → Test Accounts
    • Logged in with this account on the iPhone

What I’ve Tried


Apple’s sample code to generate a signature

Downloaded from

const express = require('express');
const router = express.Router();

const crypto = require('crypto');
const ECKey = require('ec-key');
const secp256k1 = require('secp256k1');
const uuidv4 = require('uuid/v4');

const KeyEncoder = require('key-encoder');
const keyEncoder = new KeyEncoder('secp256k1');

const fs = require('fs');

function getKeyID() {
    return "KEYIDXXXXX";
}

router.post('/offer', function(req, res) {
    const appBundleID = req.body.appBundleID;
    const productIdentifier = req.body.productIdentifier;
    const subscriptionOfferID = req.body.offerID;
    const applicationUsername = req.body.applicationUsername;

    const nonce = uuidv4();
    const currentDate = new Date();
    const timestamp = currentDate.getTime();
    const keyID = getKeyID();

    const payload = appBundleID + '\u2063' +
                  keyID + '\u2063' +
                  productIdentifier + '\u2063' +
                  subscriptionOfferID + '\u2063' +
                  applicationUsername  + '\u2063'+
                  nonce + '\u2063' +
                  timestamp;

    // Get the PEM-formatted private key string associated with the Key ID.
    // const keyString = getKeyStringForID(keyID);
    // Read the .p8 file
    const keyString = fs.readFileSync('./SubscriptionKey_47J5826J8W.p8', 'utf8');

    // Create an Elliptic Curve Digital Signature Algorithm (ECDSA) object using the private key.
    const key = new ECKey(keyString, 'pem');

    // Set up the cryptographic format used to sign the key with the SHA-256 hashing algorithm.
    const cryptoSign = key.createSign('SHA256');

    // Add the payload string to sign.
    cryptoSign.update(payload);

    /*
        The Node.js crypto library creates a DER-formatted binary value signature,
        and then base-64 encodes it to create the string that you will use in StoreKit.
    */
    const signature = cryptoSign.sign('base64');

    /*
        Check that the signature passes verification by using the ec-key library.
        The verification process is similar to creating the signature, except it uses 'createVerify'
        instead of 'createSign', and after updating it with the payload, it uses `verify` to pass in
        the signature and encoding, instead of `sign` to get the signature.

        This step is not required, but it's useful to check when implementing your signature code.
        This helps debug issues with signing before sending transactions to Apple.
        If verification succeeds, the next recommended testing step is attempting a purchase
        in the Sandbox environment.
    */
    const verificationResult = key.createVerify('SHA256').update(payload).verify(signature, 'base64');
    console.log("Verification result: " + verificationResult)

    // Send the response.
    res.setHeader('Content-Type', 'application/json');
    res.json({ 'keyID': keyID, 'nonce': nonce, 'timestamp': timestamp, 'signature': signature });

});    

module.exports = router;

Postman request and response

Request URL: http://192.168.1.141:3004/offer
Request JSON: {
    "appBundleID":"com.app.bundleid",
    "productIdentifier":"subscription.product.id",
    "offerID":"OFFERCODE1",
    "applicationUsername":"01234b43791ea309a1c3003412bcdaaa09d39a615c379cc246f5f479760629a1"
}
Response JSON: {
    "keyID": "KEYIDXXXXX",
    "nonce": "f98f2cda-c7a6-492f-9f92-e24a6122c0c9",
    "timestamp": 1753510571664,
    "signature": "MEYCIQCnA8UGWhTiCF+F6S55Zl6hpjnm7SC3aAgvmTBmQDnsAgIhAP6xIeRuREyxxx69Ve/qjnONq7pF1cK8TDn82fyePcqz"
}

Xcode Code

func buy(_ product: SKProduct) {
    let discountOffer = SKPaymentDiscount(
        identifier: "OFFERCODE1",
        keyIdentifier: "KEYIDXXXXX",
        nonce: UUID(uuidString: "f98f2cda-c7a6-492f-9f92-e24a6122c0c9")!,
        signature: "MEYCIQCnA8UGWhTiCF+F6S55Zl6hpjnm7SC3aAgvmTBmQDnsAgIhAP6xIeRuREyxxx69Ve/qjnONq7pF1cK8TDn82fyePcqz",
        timestamp: 1753510571664)
    
    let payment = SKMutablePayment(product: product)
    payment.applicationUsername = "01234b43791ea309a1c3003412bcdaaa09d39a615c379cc246f5f479760629a1"
    payment.paymentDiscount = discountOffer
    SKPaymentQueue.default().add(payment)
}

Issue

Even following instructions to the documentation and attempting various combinations, the offer keeps failing with SKErrorCodeDomain error 18.

Has anyone else experienced this? Any suggestions as to what may be amiss or how it can be corrected?

Please file a Feedback report using Feedback Assistant.

Problem Generating Signature for Subscription Offers – Error Code 18
 
 
Q