How to Verify Apple In‑App Purchases with PHP: A Step‑by‑Step Guide

This article explains how to implement Apple In‑App Purchase receipt verification in a PHP backend, covering required Apple documentation, the verification workflow, detailed PHP code for handling production and sandbox environments, and how to record successful transactions in the database.

Laravel Tech Community
Laravel Tech Community
Laravel Tech Community
How to Verify Apple In‑App Purchases with PHP: A Step‑by‑Step Guide

Apple In‑App Purchase receipt verification

Apple requires the server to send the base64‑encoded receipt received from an iOS app to the verifyReceipt endpoint and interpret the JSON response. Two endpoints are provided:

Production: https://buy.itunes.apple.com/verifyReceipt Sandbox: https://sandbox.itunes.apple.com/verifyReceipt The HTTP POST body must be a JSON object containing the key "receipt-data" with the receipt string. Apple returns a JSON object that always includes a status field. A status of 0 means the receipt is valid. A status of 21007 indicates that a sandbox receipt was sent to the production endpoint; the request should be retried using the sandbox URL.

PHP (Laravel) implementation

The following controller demonstrates the complete workflow: receiving the receipt from the iOS client, verifying it with Apple (including sandbox fallback), extracting the latest in‑app purchase record, updating the user's virtual balance, recording the transaction, and returning a standardized result.

<?php
namespace App\Http\Controllers\Main;

use App\Models\ReferInfo;
use App\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class AppleController extends Controller
{
    const FLAG = true; // true = sandbox mode, false = production

    /**
     * Verify an Apple receipt.
     * @param string $receipt_data Base64‑encoded receipt
     * @return array Result with status, message, product_id and transaction_id
     */
    public function appleValidatePay($receipt_data)
    {
        if (strlen($receipt_data) < 20) {
            return array_format('Invalid parameter', 414);
        }
        $html = $this->acurl($receipt_data);
        $data = json_decode($html, true);
        // Retry with sandbox endpoint if needed
        if ($data['status'] == '21007') {
            $html = $this->acurl($receipt_data, 1);
            $data = json_decode($html, true);
            $data['sandbox'] = '1';
        }
        if (intval($data['status']) === 0) {
            $order = $data['receipt']['in_app'];
            $k = count($order) - 1;
            $need = $order[$k];
            $result = [
                'status' => true,
                'message' => 'Purchase successful',
                'product_id' => substr($need['product_id'], 19),
                'transaction_id' => $need['transaction_id'],
            ];
        } else {
            $result = [
                'status' => false,
                'message' => 'Purchase failed status:' . $data['status'],
            ];
        }
        return $result;
    }

    /**
     * Endpoint called by the iOS client.
     * Expects POST parameters: receipt_data (string) and user_inte (int).
     */
    public function paypalApp(Request $request)
    {
        $receipt_data = $request->input('receipt_data', '');
        $user_inte = $request->user_inte; // points / coins to add
        $uid = \Auth::user()->uid;
        $user = User::lockForUpdate()->where('uid', $uid)->first();
        if (!$user) {
            return array_format('Failed to get payment object', 414);
        }
        if (self::FLAG) {
            $result = $this->appleValidatePay($receipt_data);
            if (!$result['status']) {
                return array_format('Verification failed', 414);
            }
            $transaction_id = $result['transaction_id'];
        } else {
            $transaction_id = date('ymdHis') . rand_num(10);
        }
        $order_id = order_id();
        DB::beginTransaction();
        $user->user_inte += $user_inte;
        if (!$user->save()) {
            DB::rollBack();
            return array_format('Failed to update user info', 414);
        }
        $data = [
            'uid' => $uid,
            'order_id' => $order_id,
            'user_inte' => $user_inte,
            'device' => constant('DF_DEVICE'),
            'status' => 1,
            'trade_no' => $transaction_id,
            'created_at' => date('Y-m-d H:i:s'),
        ];
        $temp = DB::table('pay_info')->insert($data);
        if (!$temp) {
            DB::rollBack();
            return array_format('Order failed', 414);
        }
        DB::commit();
        return array_format('Payment successful', 200);
    }

    /**
     * Send the receipt to Apple for verification.
     * @param string $receipt_data
     * @param int $sandbox 0 = production, 1 = sandbox
     * @return string Raw JSON response from Apple
     */
    public function acurl($receipt_data, $sandbox = 0)
    {
        $postFields = json_encode(['receipt-data' => $receipt_data]);
        $urlBuy = "https://buy.itunes.apple.com/verifyReceipt";
        $urlSandbox = "https://sandbox.itunes.apple.com/verifyReceipt";
        $url = $sandbox ? $urlSandbox : $urlBuy;
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
        $result = curl_exec($ch);
        curl_close($ch);
        return $result;
    }
}

Testing the integration

The iOS client should POST the base64 receipt as the receipt_data parameter to the paypalApp endpoint. The server will verify the receipt with Apple, update the user's virtual balance, record the transaction in the pay_info table, and return a JSON response indicating success (HTTP 200) or failure (HTTP 414).

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendPHPApplecURLIn-App Purchasereceipt verificationPayment IntegrationLaravel
Laravel Tech Community
Written by

Laravel Tech Community

Specializing in Laravel development, we continuously publish fresh content and grow alongside the elegant, stable Laravel framework.

0 followers
Reader feedback

How this landed with the community

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.