How to Fix iOS Push Notification PHP Error

I remember the first time I tried to send an iOS Push Notification using PHP. It looked simple enough just open a socket to Apple’s servers, send the payload, and done. But instead of a successful notification, I was greeted with the dreaded:

SSL3_READ_BYTES:sslv3 alert handshake failure
Failed to enable crypto
unable to connect to ssl://gateway.sandbox.push.apple.com:2195
fwrite() expects parameter 1 to be resource, boolean given
fclose() expects parameter 1 to be resource, boolean given

At first, I thought maybe my PHP was wrong. Let’s look at the code I started with:

<?php
$streamContext= stream_context_create();
stream_context_set_option($streamContext , 'ssl','local_cert' , 'TestPushApp.pem');
//stream_context_set_option($streamContext , 'ssl' , 'passphrase','password');
$socketClient = stream_socket_client('ssl://gateway.sandbox.push.apple.com:2195',$error,$errorString,60,STREAM_CLIENT_CONNECT,$streamContext);
$payload['aps']= array('alert' => 'Erste Push Nachricht ueber PHP','sound' => 'default','badge' => '20');
$payload= json_encode($payload);
echo $payload;
$deviceToken = str_replace(' ','','XXXXXXXXXXXXXXXXXX');
$message= pack('CnH*',0,32,$devicetoken);
$message= $message . pack ('n',strlen($payload));
$message= $messgae .  $payload;
fwrite($socketClient,$message);
fclose($socketClient);
?>

It looked fine, but it blew up. Let me explain what actually went wrong.

What That Error Actually

The line sslv3 alert handshake failure means APNs (Apple Push Notification service) rejected my TLS handshake. This is the secure negotiation step before data can flow. A few reasons why:

  1. Wrong or incomplete certificate
    My TestPushApp.pem didn’t have both the certificate and private key inside. Apple needs both.
  2. Environment mismatch
    Sandbox device tokens need sandbox certificates and the sandbox gateway (gateway.sandbox.push.apple.com). Production tokens need production certs and the production gateway. Mixing them fails.
  3. Server TLS problems
    Many cheap shared hosts (like square7.ch in my case) don’t allow outbound port 2195, or they run an old OpenSSL that can’t handle TLS 1.2. Apple rejects those connections.
  4. Typos in my code
    I had $devicetoken instead of $deviceToken, and $messgae instead of $message. Silly, but it matters.

Because the socket never opened, fwrite() and fclose() complained—they were trying to write to false.

Fix the Legacy (Binary) Approach

Apple’s old binary API still works in some cases (though deprecated). I fixed the typos and improved the code like this:

<?php
$certPath     = __DIR__ . '/TestPushApp.pem'; // must contain cert + private key
$passphrase   = '';                           // set if your key has one
$useSandbox   = true;                         // false for production
$apnsHost     = $useSandbox ? 'ssl://gateway.sandbox.push.apple.com:2195'
                            : 'ssl://gateway.push.apple.com:2195';
$deviceToken  = preg_replace('/\s+/', '', 'YOUR_HEX_DEVICE_TOKEN_HERE'); // 64 hex chars

// Build payload
$payload = json_encode([
  'aps' => [
    'alert' => 'Erste Push Nachricht über PHP',
    'sound' => 'default',
    'badge' => 20,
  ],
], JSON_UNESCAPED_UNICODE);

// TLS context
$ctx = stream_context_create();
stream_context_set_option($ctx, 'ssl', 'local_cert', $certPath);
if ($passphrase !== '') {
  stream_context_set_option($ctx, 'ssl', 'passphrase', $passphrase);
}

$errNo = 0;
$errStr = '';
$fp = @stream_socket_client(
  $apnsHost,
  $errNo,
  $errStr,
  60,
  STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT,
  $ctx
);

if (!$fp) {
  die("APNs connect failed ($errNo): $errStr\n");
}

$tokenBin = pack('H*', $deviceToken);
$msg = chr(0) . pack('n', 32) . $tokenBin . pack('n', strlen($payload)) . $payload;

$result = fwrite($fp, $msg);
fclose($fp);

echo $result ? "Sent.\n" : "Failed to write to socket.\n";
?>

This solved the typos, and on servers with modern TLS and open ports, it works.

Moving to the Modern Way: HTTP/2 APNs

But let me be honest binary APNs is dead. Apple now wants HTTP/2 on port 443 (much more reliable, and hosts don’t block it). With PHP and cURL, I can do it like this:

<?php
function sendApnsHttp2WithCert(
  string $deviceTokenHex,
  array  $apsPayload,
  string $bundleId,
  string $pemPath,
  string $pemPassphrase = '',
  bool   $sandbox = true
): array {
  $host = $sandbox ? 'https://api.sandbox.push.apple.com'
                   : 'https://api.push.apple.com';

  $deviceTokenHex = strtolower(preg_replace('/\s+/', '', $deviceTokenHex));
  $url = sprintf('%s/3/device/%s', $host, $deviceTokenHex);

  $payload = json_encode(['aps' => $apsPayload], JSON_UNESCAPED_UNICODE);

  $ch = curl_init($url);
  curl_setopt_array($ch, [
    CURLOPT_HTTP_VERSION     => CURL_HTTP_VERSION_2_0,
    CURLOPT_POST             => true,
    CURLOPT_POSTFIELDS       => $payload,
    CURLOPT_HTTPHEADER       => [
      'apns-topic: ' . $bundleId,
      'content-type: application/json',
    ],
    CURLOPT_SSLCERT          => $pemPath,
    CURLOPT_SSLCERTPASSWD    => $pemPassphrase,
    CURLOPT_TIMEOUT          => 20,
    CURLOPT_RETURNTRANSFER   => true,
  ]);

  $body   = curl_exec($ch);
  $err    = curl_error($ch);
  $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  curl_close($ch);

  if ($err) {
    return ['ok' => false, 'http' => 0, 'error' => $err, 'body' => $body];
  }
  return ['ok' => ($status === 200), 'http' => $status, 'body' => $body];
}

// Example use
$result = sendApnsHttp2WithCert(
  'YOUR_HEX_DEVICE_TOKEN_HERE',
  ['alert' => 'Hallo von HTTP/2!', 'sound' => 'default', 'badge' => 1],
  'com.example.yourapp',
  __DIR__ . '/TestPushApp.pem',
  '',
  true
);

var_dump($result);
?>

With this, I got proper HTTP status codes and error messages back from Apple. Debugging became much easier.

Extra Practice Making It Reusable

I didn’t stop there. To make my project cleaner, I added:

  • Environment switcher (sandbox vs production)
  • Device token validation (must be 64 hex characters)
  • Retries + logging (in case of 500/503 errors)
  • Richer payloads (categories, custom keys, mutable content)

This turned my test script into a small but reusable notification service.

Final Thought

When I first hit the SSL handshake error, I thought my PHP was broken. But the truth was bigger: I was fighting against typos, certificates, and a deprecated API. Fixing the old binary approach works if you’re on the right host, but the real solution is moving to Apple’s HTTP/2 push service.

Related blog posts