API Integration
What this is for
Loopr exposes a GraphQL API so anything that can make an HTTP request can push items into your loop, complete them, snooze them, or read what’s in there. You don’t need to sync a new external service through a Stream — if you can write a webhook, you can write to the loop.
Typical uses:
- A Zapier zap that turns a new row in Airtable into a loop item.
- A shell alias on your laptop that creates an item from the command line.
- A CI job that opens an urgent item when a build fails.
- A custom scraper that replaces a Stream Loopr doesn’t support natively.
The two pieces
- An API key — a secret you send on every request so Loopr knows it’s you. Create them in Preferences → API Keys.
- The GraphQL endpoint —
POST https://loopr.life/gql. One URL, one schema, every operation.
There’s also a live playground at /gql/playground for poking around
the schema and running queries against your own account.
Creating an API key
In preferences, name the key (e.g. macbook, ci, zapier) and pick an
expiration (1 day up to 1 year, or Never). You’ll see the plaintext key
exactly once when it’s created. Copy it somewhere safe — Loopr only
stores a hash, so there is no way to retrieve it afterwards. If you lose
it, revoke it and make a new one.
Keys look like loopr_<random>_<checksum>. The last four characters
are shown in the UI so you can tell keys apart without revealing them.
Revoke any key from the same list with one click.
Scope: a key acts as you. It can do anything your logged-in session can do — no finer-grained permissions. Treat it like a password; never commit one to a repo.
Making a request
Send the key in the Authorization: Bearer header:
curl -X POST https://loopr.life/gql \
-H "Authorization: Bearer loopr_…" \
-H "Content-Type: application/json" \
-d '{"query":"{ listQueuedItems(limit: 3) { results { id title urgency } } }"}'
Browse the full schema at /gql/playground — schema introspection works
there, autocomplete included. All queries and mutations accept the same
Authorization header.
Common operations
Create an item
Push something into your loop. customSourceName is a free-form label
(e.g. zapier, my-script) that shows up as a little uppercase badge on
the item so you remember where it came from.
mutation {
createItem(input: {
title: "Review Q2 budget"
customSourceName: "zapier"
urgency: 3
}) {
result { id title urgency }
errors { message }
}
}
List queued items
{
listQueuedItems(limit: 20) {
results {
id
title
urgency
customSourceName
snoozedUntil
}
count
}
}
Equivalent queries exist for snoozed items (listSnoozedItems) and
top-of-mind items (listTopOfMindItems).
Complete, snooze, requeue
mutation {
completeItem(id: "...") { result { id state } errors { message } }
}
mutation {
snoozeItem(id: "...", until: "2026-05-01T09:00:00Z") {
result { id snoozedUntil } errors { message }
}
}
unsnoozeItem, requeueItem, setItemUrgency, updateItem, and
destroyItem all follow the same shape.
Streams
{ listStreams { results { id title type paused } } }
pauseStream, resumeStream, and deleteStream are available as
mutations.
The customSourceName field
The built-in source field (github, gmail, native, …) is fixed by
the Stream type that made the item. customSourceName is the escape
hatch for API-created items: a string you choose that shows up as a badge
on the item so you can tell at a glance which integration it came from.
Pick short, lowercase names (zapier, home-assistant, ci-bot). It’s
optional — omit it and the item just shows up with no extra badge.
Non-goals
The API does not currently support:
- OAuth or per-scope permissions. An API key is equivalent to a session.
- Webhooks out of Loopr (subscribing to item events from an external service). If you need that, let us know.
- Rate limit headers. Be reasonable; we’ll reach out if a key is noisy.
Gotchas
- Lost keys are lost. Regenerate and update your callers.
- “Expires: Never” keys are convenient for long-lived integrations but worth treating with extra care — if the laptop with a never-expiring key goes missing, revoke from Preferences.
- The GraphQL schema evolves. Fields we add are additive; we’ll announce breaking changes in the changelog. Pinning a client? Query only the fields you need and handle unknown ones gracefully.