Backend Development 7 min read

Implementing iOS In‑App Purchase Subscription Verification and Renewal Handling in PHP

This article walks through the server‑side implementation of iOS In‑App Purchase (IAP) subscription verification, order creation, and automatic renewal handling using PHP, including detailed code for receipt validation, sandbox/production switching, and processing Apple server notifications.

php中文网 Courses
php中文网 Courses
php中文网 Courses
Implementing iOS In‑App Purchase Subscription Verification and Renewal Handling in PHP

The author shares a practical guide for integrating iOS In‑App Purchase (IAP) subscription payments into a company's app, focusing on the backend logic written in PHP.

Step 1 – Order creation and receipt verification : The client sends a receipt token, which the server validates against Apple’s verification endpoint (sandbox or production). If the receipt is valid, an order is generated and returned to the client.

<code>public function pay()
{
    $uid = $this->request->header('uid');
    $receipt_data = $this->request->post('receipt');
    if (!$uid || !$receipt_data) return $this->rep(400);

    $info = $this->getReceiptData($receipt_data, $this->isSandbox); // verify with Apple
    Log::info(['uid'=>$uid,'receipt'=>$receipt_data,'iap_info'=>$info]);

    if (is_array($info) && $info['status'] == 0) {
        // process order creation logic
    } elseif (is_array($info) && $info['status'] == 21007) {
        $new_info = $this->getReceiptData($receipt_data, true); // sandbox re‑verify
        // process order creation logic
    }
}</code>
<code>private function getReceiptData($receipt, $isSandbox = false)
{
    if ($isSandbox) {
        $endpoint = 'https://sandbox.itunes.apple.com/verifyReceipt'; // sandbox
    } else {
        $endpoint = 'https://buy.itunes.apple.com/verifyReceipt'; // production
    }
    $postData = json_encode(['receipt-data' => $receipt, 'password' => 'abde7d535c']);
    $ch = curl_init($endpoint);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
    $response = curl_exec($ch);
    $errno = curl_errno($ch);
    curl_close($ch);
    if ($errno != 0) {
        $order['status'] = 408; // curl error
    } else {
        $data = json_decode($response, true);
        if (isset($data['status'])) {
            $order = isset($data['receipt']) ? $data['receipt'] : [];
            $order['status'] = $data['status'];
        } else {
            $order['status'] = 30000;
        }
    }
    return $order;
}</code>

Step 2 – Automatic subscription renewal handling : Apple sends server‑to‑server notifications for renewal events. The backend provides an endpoint that parses the notification, validates the shared secret, extracts the latest receipt information, and records renewal or cancellation events.

<code>/* Automatic renewal subscription callback */
public function renew()
{
    $resp_str = $this->request->post();
    Log::info(['resp_str'=>$resp_str]);
    if (!empty($resp_str)) {
        $data = $resp_str['unified_receipt'];
        $notification_type = $resp_str['notification_type']; // e.g., INITIAL_BUY, RENEWAL, CANCEL
        $password = $resp_str['password'];
        if ($password == "abde7d5353") {
            $receipt = isset($data['latest_receipt_info']) ? $data['latest_receipt_info'] : $data['latest_expired_receipt_info'];
            $receipt = self::arraySort($receipt, 'purchase_date', 'desc');
            $original_transaction_id = $receipt['original_transaction_id'];
            $transaction_id = $receipt['transaction_id'];
            $purchaseDate = str_replace(' America/Los_Angeles','',$receipt['purchase_date_pst']);
            $orderinfo = Order::field('uid,original_transaction_id,money,order_no,pay_time')
                ->where(['original_transaction_id' => $original_transaction_id])->find();
            $user_info = User::field('app_uid,device_id,unionid')->get($orderinfo['uid']);
            if ($notification_type == 'CANCEL') {
                IpaLog::addLog($orderinfo['uid'], $orderinfo['order_no'], $receipt, $resp_str);
            } else {
                if (in_array($notification_type, ['INTERACTIVE_RENEWAL','RENEWAL','INITIAL_BUY'])) {
                    IapRenew::addRenew($orderinfo['uid'], $receipt, $data['latest_receipt'], 1, $notification_type, $user_info['app_uid'], $purchaseDate);
                } else {
                    IapRenew::addRenew($orderinfo['uid'], $receipt, $data['latest_receipt'], 0, $notification_type, $user_info['app_uid'], $purchaseDate);
                }
                IpaLog::addLog($orderinfo['uid'], $orderinfo['order_no'], $receipt, $resp_str);
            }
        } else {
            Log::info('通知传递的密码不正确--password:' . $password);
        }
    }
}

private function toTimeZone($src, $from_tz = 'Etc/GMT', $to_tz = 'Asia/Shanghai', $fm = 'Y-m-d H:i:s')
{
    $datetime = new \DateTime($src, new \DateTimeZone($from_tz));
    $datetime->setTimezone(new \DateTimeZone($to_tz));
    return $datetime->format($fm);
}

private static function arraySort($arr, $key, $type='asc')
{
    $keyArr = [];
    foreach ($arr as $k=>$v){
        $keyArr[$k] = $v[$key];
    }
    if($type == 'asc'){
        asort($keyArr);
    } else {
        arsort($keyArr);
    }
    foreach ($keyArr as $k=>$v){
        $newArray[$k] = $arr[$k];
    }
    $newArray = array_merge($newArray);
    return $newArray[0];
}</code>

The article concludes with a reminder to share and bookmark the guide.

backendiOSPHPAppleSubscriptionIn-App Purchase
php中文网 Courses
Written by

php中文网 Courses

php中文网's platform for the latest courses and technical articles, helping PHP learners advance quickly.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.