Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Pinboard Api Laravel Package

kijin/pinboard-api

A Laravel/PHP client for the Pinboard bookmarking API, making it easy to fetch, create, update, and delete bookmarks and tags with a clean, expressive wrapper. Ideal for syncing bookmarks or building Pinboard-powered tools.

Deep Wiki
Context7

Getting Started

kijin/pinboard-api is a thin, single-class PHP wrapper around the Pinboard.in v1 REST API (https://api.pinboard.in/v1/). It has no Laravel integration, no service provider, and effectively one dependency-free client object. Treat it as a low-level transport, not a framework package.

First steps

  1. Read the package source before the README — with a project this small the class file is the documentation. Look at the constructor (it takes credentials or a token) and the list of public methods; they map almost 1:1 to Pinboard endpoints (posts/add, posts/get, posts/recent, posts/all, posts/update, tags/get, tags/rename, tags/delete, user/api_token). Method names and argument order vary between versions, so confirm against vendor/kijin/pinboard-api/src/ rather than blog posts.
  2. Get an API token from https://pinboard.in/settings/password (the format is username:HEXTOKEN). Do not use your account password — Pinboard has deprecated password query params, and the package may still default to them.
  3. Store the token in config/services.php and .env, not in code:
// config/services.php
'pinboard' => [
    'token' => env('PINBOARD_TOKEN'), // "user:ABCDEF..."
],
  1. Verify connectivity with the cheapest possible call. posts/update returns a single timestamp and mutates nothing — ideal for a smoke test in tinker:
$client = new \Kijin\Pinboard\Pinboard(config('services.pinboard.token'));
$client->getLastUpdate(); // or the method wrapping posts/update

First real use case: a "save for later" button on an internal Laravel app. A controller accepts a URL + title, dispatches a queued job, and the job calls posts/add. That single flow exercises auth, tag formatting, the 3-second rate rule, and error handling — everything you need to learn before scaling up to sync.

Implementation Patterns

Bind the client as a singleton behind your own interface. Never let the vendor class leak into controllers or jobs. Wrap it:

// app/Services/Pinboard/PinboardClient.php (interface)
interface PinboardClient {
    public function add(string $url, string $title, array $options = []): void;
    public function recent(int $count = 20): array;
    public function tags(): array;
    public function lastUpdate(): string;
}
// app/Providers/PinboardServiceProvider.php
$this->app->singleton(PinboardClient::class, fn () => new VendorPinboardAdapter(
    config('services.pinboard.token')
));

This gives you one place to swap in Laravel's Http client if the package breaks, and one place to fake in tests.

Queue all writes. Pinboard explicitly asks clients to wait at least 3 seconds between API calls, and the package almost certainly does no throttling. Push mutations through jobs with a rate-limited queue:

// App\Jobs\AddBookmark
public function handle(PinboardClient $pinboard): void
{
    $pinboard->add($this->url, $this->title, [
        'tags' => implode(' ', $this->tags), // space-delimited string
        'toread' => $this->unread,
        'shared' => false, // be explicit; defaults are easy to misread
    ]);
}
// AppServiceProvider
RateLimiter::for('pinboard', fn () => Limit::perMinute(20)); // 1 per 3s

Apply ->middleware(['throttle:pinboard']) in the job constructor or use RateLimited job middleware.

Incremental sync with the posts/update watermark. Don't diff your posts with posts/all on every run. Store the timestamp from posts/update, then fetch changed posts. posts/all is paginated (start / results) and slow — cache it or run it on a nightly schedule only. Persist url, description, extended, tags, dt, hash, shared, toread, and meta locally so your UI never blocks on Pinboard.

Use the right read endpoint for the job:

Need Endpoint
Dashboard widget / "latest saves" posts/recent (count up to 100)
Fetch metadata for a known URL posts/get with url — returns meta plus matching posts
Tag autocomplete / tag cloud tags/get (cheap, cache 10–15 min)
Rebuild local index posts/all (paginated, slowest)
Detect changes posts/update

Cache aggressively on reads. Cache::remember('pinboard.tags', now()->addMinutes(15), ...) and similar for recent posts. Tags change rarely and the API is one request every three seconds — cache misses are expensive in wall time, not money.

Normalize URLs before saving. Strip utm_*, fbclid, fragments, and trailing slashes in a single normalizeUrl() helper. Pinboard treats URLs as keys; inconsistent normalization produces duplicates and posts/delete calls that don't hit the record you expected.

Handle errors in one adapter method. Wrap calls in try/catch, inspect both the HTTP status and the result field in the decoded JSON, and map:

  • 401 / "result":"not authorized" → config error, alert, don't retry.
  • 429 / 503 → release the job with backoff ($this->release(60 * 15)).
  • "result":"done" → success — anything else means the call did not take effect even though HTTP was 200.

Testing. Fake your own PinboardClient interface, not the vendor class. For an integration test against the real API, use a dedicated Pinboard account with a token in CI secrets, and expect it to be slow — one call per test case is the only sustainable pattern.

Gotchas and Tips

  • Use the token, not the password. Older versions of this package pass ?username=&password= which Pinboard now rejects. If the client's constructor accepts a password, it may still work via auth_token; verify by checking the query string it builds.
  • The token ends up in the query string. That means it can leak into Laravel's HTTP client logs, Log::debug($request->url()), Sentry breadcrumbs, and exception messages. Configure your request logger to strip auth_token and never dump the full URL.
  • posts/add defaults are dangerous. replace defaults to yes, so re-adding an existing URL silently overwrites the title, notes, and tags you set earlier. If you want non-destructive saves, pass replace=no explicitly. Similarly, shared=yes makes a bookmark public — a typo in a boolean-to-string conversion can publish something you meant to keep private.
  • dt must be ISO-8601 UTC and in the past. Use now()->utc()->format('Y-m-d\TH:i:s\Z'). Omitting dt means "now". Passing local time without the Z produces a shifted timestamp with no error.
  • Tags are a single space-delimited string, not an array. Commas are not separators in the API. Multi-word tags need hyphens (reading-list), and passing an array directly will likely be cast to "Array" in the query string.
  • result is authoritative. HTTP 200 with {"result_code":"something went wrong"} is a failed call. Code that only checks the HTTP status will silently lose bookmarks.
  • Response shapes are irregular. posts/get has no posts key when nothing matches; meta only appears for URLs already in the account. Always ?? [] and ?? null.
  • posts/all does not tell you about deletions. Deleted bookmarks simply disappear. Diff only after a full sync, and prefer the posts/update watermark plus periodic full reconciliations rather than assuming posts/all is a complete snapshot every page.
  • Case sensitivity. Tag matching is case-insensitive on Pinboard but preserves original casing; URL matching is effectively exact. Normalize tags to lowercase on write if you want predictable local queries.
  • Old package, old constraints. composer require may fail on PHP 8.x if composer.json pins php ^5.x or ^7.x. Check with composer why-not kijin/pinboard-api. If it's abandoned, the wrapper is small enough to replace with 40 lines of Http::withToken(...) — don't spend a day fighting Composer for a class you could rewrite in an hour.
  • No retry, no timeout, no backoff, no logging. Whatever transport the class uses (likely cURL or file_get_contents) will block the request until your PHP default_socket_timeout fires. Only ever call it from queued jobs or scheduled commands, never inline in a web request.
  • Extension point is the wrapper, not the class. If the vendor class doesn't expose a "send request" method you can override, subclassing is fragile. Keep the adapter interface as your seam, and swap the implementation when the package dies — which, at ~100 stars and no recent releases, is a reasonable planning assumption.
  • Scheduling: run the sync command hourly at most, and use withoutOverlapping() — a slow posts/all pagination run overlapping with the next tick is the quickest way to get your IP throttled.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels