PHPackages                             apermo/apermo-stash - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. apermo/apermo-stash

ActiveWordpress-plugin[API Development](/categories/api)

apermo/apermo-stash
===================

Apermo Stash — a self-hosted WordPress bookmark collection with a token-protected REST API.

v0.2.2(1mo ago)1495[19 issues](https://github.com/apermo/apermo-stash/issues)[2 PRs](https://github.com/apermo/apermo-stash/pulls)GPL-2.0-or-laterPHPPHP &gt;=8.1CI passing

Since May 2Pushed 1mo agoCompare

[ Source](https://github.com/apermo/apermo-stash)[ Packagist](https://packagist.org/packages/apermo/apermo-stash)[ RSS](/packages/apermo-apermo-stash/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (7)Dependencies (15)Versions (13)Used By (0)

Apermo Stash
============

[](#apermo-stash)

[![PHP CI](https://github.com/apermo/apermo-stash/actions/workflows/ci.yml/badge.svg)](https://github.com/apermo/apermo-stash/actions/workflows/ci.yml)[![License: GPL v2+](https://camo.githubusercontent.com/996c3451ae01accccbdbaaa15299d3a015844792e6de4a884a5d12f1356bacd4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d47504c76322b2d626c75652e737667)](LICENSE)

A self-hosted WordPress plugin for collecting links. Inspired by [linkding](https://linkding.link/). Stores URL + title + notes + tags as a custom post type and exposes a token-protected REST API so a browser extension can save links from anywhere.

Per-link public/private visibility, idempotent save (safe to re-submit), and CORS configured for `chrome-extension://*` origins out of the box.

Requirements
------------

[](#requirements)

- PHP 8.1+
- WordPress 6.4+
- Composer (development only — runtime has no Composer dependencies)
- Node.js 20+ and npm (activates husky pre-commit hook, runs Playwright)
- [DDEV](https://ddev.readthedocs.io/) (for local development)

Installation
------------

[](#installation)

1. Clone or download this repository into `wp-content/plugins/apermo-stash/`.
2. Run `composer install --no-dev` to generate the autoloader.
3. Activate the plugin through the WordPress "Plugins" screen.
4. Visit **Settings → Apermo Stash** to generate an API token (see Authentication below).

Authentication
--------------

[](#authentication)

Apermo Stash accepts two equivalent authentication schemes; pick whichever fits your client.

### WordPress Application Passwords (Basic Auth)

[](#wordpress-application-passwords-basic-auth)

Available in WordPress core. Generate one under **Users → Profile → Application Passwords** and pass it as Basic Auth:

```
curl -u "your-username:xxxx xxxx xxxx xxxx xxxx xxxx" \
     https://example.tld/wp-json/apermo-stash/v1/links
```

### Apermo Stash Bearer Tokens

[](#apermo-stash-bearer-tokens)

Better suited for browser extensions: generate at **Settings → Apermo Stash → API Tokens**. The plain token is shown **once** at creation time — copy it immediately. Send it as:

```
curl -H "Authorization: Bearer " \
     https://example.tld/wp-json/apermo-stash/v1/links
```

Each token is bound to a WordPress user; permission checks run against that user's capabilities (`edit_posts` for write endpoints).

REST API
--------

[](#rest-api)

Base path: `/wp-json/apermo-stash/v1`.

MethodPathDescription`GET``/links`List links (filters: `tag`, `q`, `unread`, `archived`, `public`/`private`, `page`, `per_page`)`POST``/links`Create a link (idempotent — same URL returns existing record with `X-Apermo-Stash-Existing: 1`)`GET``/links/{id}`Fetch a single link`PATCH``/links/{id}`Update fields`DELETE``/links/{id}`Delete a link`GET``/tags`List tags with link counts`GET``/check?url=...`Returns `{exists: bool, id?: int}` for a given URL### Examples

[](#examples)

Save a link; let the server fetch the title and description:

```
curl -X POST https://example.tld/wp-json/apermo-stash/v1/links \
     -H "Authorization: Bearer " \
     -H "Content-Type: application/json" \
     -d '{"url":"https://example.tld/article","tags":["reading"],"public":true}'
```

Check whether a URL is already saved (browser-extension "already saved" badge):

```
curl -H "Authorization: Bearer " \
     "https://example.tld/wp-json/apermo-stash/v1/check?url=https://example.tld/article"
```

Search and filter:

```
curl -H "Authorization: Bearer " \
     "https://example.tld/wp-json/apermo-stash/v1/links?tag=reading&unread=1"
```

### Public versus private links

[](#public-versus-private-links)

Links use WordPress's native `post_status`:

- `publish` (public) — readable without authentication via the REST API.
- `private` — only the owner (and users with `edit_others_posts`) can read.

Anonymous `GET /links` returns only public links. Authenticated users see their own links plus any public links owned by other users. POST, PATCH, DELETE always require authentication.

### CORS

[](#cors)

By default Apermo Stash sends CORS headers permitting `chrome-extension://*`origins. Add additional origins via the `apermo_stash_allowed_origins` filter:

```
add_filter( 'apermo_stash_allowed_origins', static function ( array $origins ): array {
    $origins[] = 'https://my-frontend.example.tld';
    return $origins;
} );
```

To **narrow** the default allow-list once you know your extension's specific ID — defense-in-depth on top of the Bearer requirement — return only that origin:

```
add_filter( 'apermo_stash_allowed_origins', static function (): array {
    return [ 'chrome-extension://abcdefghijklmnopqrstuvwxyzabcdef' ];
} );
```

### Outbound HTTP

[](#outbound-http)

Apermo Stash makes one outbound HTTP request per saved link — to the saved URL itself, via `wp_safe_remote_get` (5 s timeout, up to three redirects, all re-validated). The fetched body is parsed for `` and ``; on failure the link still saves and an "unreachable" warning is shown on next edit. `wp_safe_remote_get` blocks loopback and private IP ranges, so a hostile URL can't be used to probe internal services.

No third-party services are contacted. No analytics, no telemetry. The companion Chrome extension talks only to the host you configure on its options page.

Development
-----------

[](#development)

```
composer install
npm install               # activates husky pre-commit hook
composer cs               # PHPCS
composer cs:fix           # PHPCBF
composer analyse          # PHPStan
composer test:unit        # unit tests (Brain Monkey)
composer test:integration # integration tests (wp-phpunit)
npm run test:e2e          # Playwright E2E
```

### Local WordPress environment

[](#local-wordpress-environment)

```
ddev start && ddev orchestrate
```

License
-------

[](#license)

[GPL-2.0-or-later](LICENSE)

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance73

Regular maintenance activity

Popularity19

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~8 days

Total

7

Last Release

33d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/910b8010a35a86821d0b90d645374f5ae484513f2c195818e4c54bc0175d12e1?d=identicon)[apermo](/maintainers/apermo)

---

Top Contributors

[![apermo](https://avatars.githubusercontent.com/u/4695889?v=4)](https://github.com/apermo "apermo (140 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/apermo-apermo-stash/health.svg)

```
[![Health](https://phpackages.com/badges/apermo-apermo-stash/health.svg)](https://phpackages.com/packages/apermo-apermo-stash)
```

###  Alternatives

[exsyst/swagger

A php library to manipulate Swagger specifications

35916.4M7](/packages/exsyst-swagger)[hubspot/api-client

Hubspot API client

24016.2M20](/packages/hubspot-api-client)[pocketmine/bedrock-protocol

An implementation of the Minecraft: Bedrock Edition protocol in PHP

172445.0k18](/packages/pocketmine-bedrock-protocol)[botman/driver-telegram

Telegram driver for BotMan

93459.5k6](/packages/botman-driver-telegram)[wearerequired/rest-likes

Like posts and comments using the REST API.

136.9k](/packages/wearerequired-rest-likes)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
