---
title: 'Publishing to Apple News from PHP'
canonical: 'https://www.dealmobile.co.uk/writing/publishing-to-apple-news-from-php'
url: 'https://www.dealmobile.co.uk/writing/publishing-to-apple-news-from-php'
type: Post
published: '2026-03-21'
summary: 'What it takes to publish an article to Apple News from PHP, from signing requests and building multipart uploads to the JSON details Apple’s format is strict about.'
---

# Publishing to Apple News from PHP

Published 2026-03-21

![A diagram of PHP objects becoming a multipart POST of article.json, metadata and an image, signed with HHMAC, and sent to Apple News.](https://www.dealmobile.co.uk/sites/default/files/styles/almanac_shot_md/public/content/images/posts/publishing-to-apple-news-from-php/hero.png.webp?itok=XQvhCe3Y)

Publishing to Apple News comes in two parts. One is the Apple News Publisher API, an HTTP API with its own way of signing requests. The other is Apple News Format, or ANF, a JSON document that describes the article: its components, styles and layout. My [Apple News API](https://www.dealmobile.co.uk/projects/apple-news-api.md) library for PHP handles both. This is a walk through what each part involves, and a few details that are easy to get wrong.

## Signing requests

The API doesn’t use OAuth or a bearer token. Each request carries an `Authorization` header signed with HMAC-SHA256, using a key ID and a secret from your Apple News channel.

The string to sign is the HTTP method, the full URL and the current date in UTC, joined with nothing in between. For a POST, the content type and the whole body are added on the end. The secret arrives base64-encoded, so it’s decoded before signing, and the signature is base64-encoded again:

```php
$canonical = $method . $url . $date;

if ($method === 'POST' && $contentType !== null && $body !== null) {
    $canonical .= $contentType . $body;
}

$signature = base64_encode(hash_hmac('sha256', $canonical, base64_decode($secret, true), true));
$header = sprintf('HHMAC; key=%s; signature=%s; date=%s', $keyId, $signature, $date);

```

Two things to watch. The body you sign must be byte for byte the body you send, multipart boundaries included. And a secret that isn’t valid base64 should fail loudly, so the library decodes in strict mode and throws if it can’t.

## Uploading an article

Creating an article is a `multipart/form-data` POST to the channel. The message must contain an `article.json` part. It can also have a `metadata` part, wrapped in a `data` key, and one part for each image that the article refers to with a `bundle://` URL.

The client builds that body for you. You pass a map of bundle names to local files:

```php
$article->addComponent(Photo::fromBundle('hero.jpg'));

$client->createArticle($channelId, $article, null, [
    'bundle://hero.jpg' => '/path/to/hero.jpg',
]);

```

Updating needs one more thing: the article’s current revision token, from the last response. The client adds it to the metadata part, so the call is `updateArticle($articleId, $revision, $article)`. If the article has changed since, Apple answers with a 409 conflict, and you fetch a fresh revision and try again.

## Building the document

ANF documents can be large: dozens of component types, named text styles, component styles, layouts, animations and conditional rules for dark mode or narrow screens. The library models all of it as PHP objects with a fluent interface:

```php
$article = Article::create(identifier: 'story-123', title: 'A headline', language: 'en');

$article
    ->addComponent(new Title('A headline'))
    ->addComponent(new Heading('Background', level: 2))
    ->addComponent((new Body('The <strong>first</strong> paragraph.'))->setFormat('html'));

```

Each object knows its own ANF `role` and serialises itself. The article collects them, along with its styles and metadata, into the final document.

## The empty object problem

This week’s release, 2.0.1, fixed a PHP problem that shows up with any strict JSON API. PHP has one array type for lists and maps, so `json_encode([])` produces `[]`. But in ANF an empty layout or style is an object, and Apple expects `{}`. Send `[]` and the document is invalid.

The fix walks the finished data and swaps every empty array for an empty `stdClass`, which encodes as `{}`:

```php
private static function convertEmptyArraysToObjects(mixed $data): mixed
{
    if (is_array($data)) {
        if (empty($data)) {
            return new \stdClass();
        }
        return array_map(self::convertEmptyArraysToObjects(...), $data);
    }
    return $data;
}

```

## When Apple says no

Apple’s error responses name the problem and the field: an error code such as `INVALID_DOCUMENT` and a key path such as `components[0].text`. The library turns them into an `AppleNewsException` with `getErrorCode()` and `getKeyPath()`, and a separate `AuthenticationException` for 401 and 403.

## Bring your own HTTP client

The library doesn’t ship an HTTP client. It depends on the PSR-18 client and PSR-17 factory interfaces, so it works with Guzzle, Symfony’s client or whatever your application already uses, and has no framework dependency. It needs PHP 8.1 or later. PHPUnit tests and PHPStan at level 8 run on every push.

```bash
composer require tomgould/apple-news-api guzzlehttp/guzzle guzzlehttp/psr7

```

The [documentation](https://applenews.dealmobile.co.uk) covers every class, and the code is on [GitHub](https://github.com/tomgould/Apple-News-API).

- **Projects:** [Apple News API](https://www.dealmobile.co.uk/projects/apple-news-api.md)
- **Tags:** [PHP](https://www.dealmobile.co.uk/writing/tag/php.md), [Apple News](https://www.dealmobile.co.uk/writing/tag/apple-news.md), [APIs](https://www.dealmobile.co.uk/writing/tag/apis.md), [Publishing](https://www.dealmobile.co.uk/writing/tag/publishing.md), [Apple News API](https://www.dealmobile.co.uk/writing/tag/apple-news-api.md)

---

Index of this site: [llms.txt](https://www.dealmobile.co.uk/llms.txt)
