Numikeep
How it worksLegacy planPrivacyFAQLog inGet early access

Developers

Connect your app to Numikeep

Numikeep lets a collector keep every coin, its value and where it's kept in one place. If your app already knows about someone's coins — a dealer inventory, a grading submission, a purchase — you can send those coins straight into their Numikeep collection, with their permission, so they never type them twice.

How access works

There are two ways in, and both end in the same kind of connection:

  • A connection key — the collector creates it in Numikeep → Settings → Connected apps and pastes it into your app. Best for personal tools, scripts and AI agents.
  • Connect with Numikeep — a sign-in and approval screen, like “Sign in with Google”. Best for apps with many users. Apps are registered by Snowpack; write to us to register yours.

What a connection can and can't do

These limits are enforced by the server, not left to your good behaviour:

  • Add coins (permission coins:write, always granted): add coins, storage locations and photos, and change or remove only what your connection added. Coins the collector entered, or another app added, answer 404 to you exactly as if they did not exist.
  • See coins (permission coins:read, only if the collector ticks it): what each coin is — name, grade, grading service, certification, CAC, quantity, copper colour. Nothing else.
  • Never: where coins are kept, what was paid, notes, photos, the heir plan, dealers, or anything about the account. A connection cannot sign in, create other connections, or approve apps. Where a collection is kept never leaves the collector — that is the rule this whole design is built around.
  • Limits: 120 requests a minute and 5,000 writes a day per connection; request bodies up to 256KB; photos up to 25MB each (JPEG, PNG, WebP, HEIC), 200 a day and 5GB in total per connection.
  • The collector can disconnect you at any moment, and it takes effect on your next request. Resetting their password or signing in with their backup phrase disconnects every app, so be ready to ask them to connect again.
  • Creating a key or approving an app requires the collector to have signed in within the last ten minutes — a stolen session can't be turned into a lasting connection.
  • When your connection re-saves one of its coins, fields you don't send keep their current value — you can't accidentally erase a note or a price the collector added.

Quickstart: add a coin in one request

Send the key as a bearer token. Give each coin a UUID you keep: sending the same id again updates that coin instead of adding a second one.

curl https://numikeep-api-824632661809.us-east1.run.app/v2/collection/coins \
  -H "Authorization: Bearer $NUMIKEEP_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "3f0c6a1e-2b4d-4e8f-9a7b-1c2d3e4f5a6b",
    "catalogItemId": "1893-S Morgan Dollar",
    "grader": "PCGS", "grade": "VF30",
    "cac": true, "cacTier": "green",
    "cert": "12345678",
    "acquiredDate": "2026-09-01", "acquiredPriceCents": 450000
  }'

catalogItemId is what the coin is, in plain words. If you know its exact Greysheet id, end the name with (GS 6207) — Numikeep will value exactly that variety instead of reading the words.

JavaScript

import { randomUUID } from 'node:crypto';

const API = 'https://numikeep-api-824632661809.us-east1.run.app';
const headers = {
  authorization: `Bearer ${process.env.NUMIKEEP_KEY}`,
  'content-type': 'application/json',
};

async function numikeep(method, path, body) {
  const res = await fetch(API + path, {
    method, headers, body: body && JSON.stringify(body),
    redirect: 'error', // never let a redirect carry the key elsewhere
  });
  const data = await res.json();
  if (!res.ok) throw new Error(`Numikeep ${res.status}: ${data.error}`);
  return data;
}

const coin = await numikeep('POST', '/v2/collection/coins', {
  id: randomUUID(), catalogItemId: '1909-S VDB Lincoln Cent',
  grader: 'NGC', grade: 'MS64', color: 'RB',
});

Python

import os, uuid, requests

API = "https://numikeep-api-824632661809.us-east1.run.app"
S = requests.Session()
S.headers["Authorization"] = f"Bearer {os.environ['NUMIKEEP_KEY']}"

def numikeep(method, path, body=None):
    r = S.request(method, API + path, json=body, allow_redirects=False, timeout=30)
    if not r.ok:
        raise RuntimeError(f"Numikeep {r.status_code}: {r.json().get('error')}")
    return r.json()

coin = numikeep("POST", "/v2/collection/coins", {
    "id": str(uuid.uuid4()), "catalogItemId": "1880-CC Morgan Dollar",
    "grader": "PCGS", "grade": "MS65",
})

Adding a photo

Two steps: ask for an upload address, then send the bytes straight to it. The address only accepts that exact size and type, and expires in minutes.

const bytes = await fs.readFile('obverse.jpg');
const { uploadUrl, headers: uploadHeaders } = await numikeep('POST', '/v2/collection/photos', {
  coinId: coin.id, side: 'obverse',          // obverse | reverse | slab | other
  contentType: 'image/jpeg', contentLength: bytes.length,
});
await fetch(uploadUrl, { method: 'PUT', headers: uploadHeaders, body: bytes });

Reference

  • GET /v2/collection — { collection: { id } }; with coins:read, also coins: id, catalogItemId, grade, grader, graderName, cac, cacTier, cert, qty, color, createdAt, updatedAt.
  • POST /v2/collection — create the collection if the collector has none.
  • POST /v2/collection/coins — add or update a coin your connection added. Fields: id (UUID), catalogItemId (required), grader(RAW, PCGS, NGC, CACG, ANACS, ICG, OTHER), grade (required unless RAW), graderName (with OTHER), cac, cacTier (green | gold), cert, qty, color (RD | RB | BN), acquiredDate, acquiredPriceCents, locationId (one your connection added), notes. Unknown fields are refused.
  • DELETE /v2/collection/coins/:id — remove a coin your connection added.
  • POST /v2/collection/locations — { id, label, kind }, kind one of home_safe, bank_box, dealer, display, other. Private to the collector.
  • POST /v2/collection/photos — see above. DELETE /v2/collection/photos/:idremoves one your connection added.

Errors are JSON, { "error": "code" }: 400 invalid input, 401 key not valid (revoked, replaced or expired), 403 not allowed for connections, 404 not found or not yours, 413 too large, 429 slow down (per-minute) or daily_limit_reached.

Connect with Numikeep (OAuth 2.1)

For apps with many users. Standard authorization code flow with PKCE (S256) — required for every app. Once Snowpack registers your app you receive a client_id (and a client_secret for server-side apps) and your exact redirect addresses are recorded; nothing else is accepted.

  1. Send the collector to https://app.numikeep.com/connect?client_id=…&redirect_uri=…&scope=coins:write&state=…&code_challenge=…&code_challenge_method=S256. They sign in and see exactly what you asked for.
  2. They come back to your redirect_uri with code and your state (check it matches), or with error=access_denied.
  3. Within 60 seconds, exchange the code once:
curl https://numikeep-api-824632661809.us-east1.run.app/v2/oauth/token \
  -d grant_type=authorization_code -d code=$CODE \
  -d redirect_uri=https://yourapp.example/numikeep/callback \
  -d client_id=$CLIENT_ID -d client_secret=$CLIENT_SECRET \
  -d code_verifier=$CODE_VERIFIER
# → { access_token: "nkat_…", expires_in: 3600, refresh_token: "nkrt_…", scope: "coins:write" }

Use the access token exactly like a key. It lasts one hour; renew it with grant_type=refresh_token. Each refresh token works once and is replaced every time — present a used one and every token for that collector is revoked, on the assumption it was stolen. POST /v2/oauth/revoke ends a connection from your side.

For AI agents: the Numikeep MCP server

Lets an assistant like Claude add coins for a collector — “add the twelve coins on this invoice”. It runs on the collector's own computer with their key, and can only do what the key allows. It can upload photos only from one folder the collector chooses, and only real image files, so an assistant that has been tricked by something it read can't send other files anywhere.

{
  "mcpServers": {
    "numikeep": {
      "command": "npx",
      "args": ["-y", "numikeep-mcp"],
      "env": {
        "NUMIKEEP_KEY": "nkpat_…",
        "NUMIKEEP_PHOTO_DIR": "/Users/you/Pictures/Coins"
      }
    }
  }
}

The package is numikeep-mcp on npm; it needs Node 20 or later.

Tools: numikeep_status, add_coin, update_coin, remove_coin, add_location, add_photo, list_coins (only with the see-coins permission).

Developer rules

By connecting an app to Numikeep you agree to these, as part of our terms (§13). We may disable an app that breaks them.

  • Say who you are. Your app's name and website must be accurate, and you must never present yourself as Numikeep or Snowpack.
  • Ask only for what you need, and use it only for what the collector connected you for. Don't sell, rent or share anything you receive, and delete it when the collector disconnects you or asks you to.
  • Never try to learn where a collector's coins are kept, or work around a limit, a permission or another app's data.
  • Keep keys, tokens and your client secret secret — server-side only.
  • Tell us about security problems you find, and don't exploit them.

Keeping keys safe

  • Keys start nkpat_, access tokens nkat_, refresh tokens nkrt_ — easy to spot in a log or a secret scanner. Keep them server-side; never put one in a web page or a mobile app bundle.
  • If a key may have leaked, the collector can use Replace key in Settings: the old one stops at once and your connection keeps the coins it added.
  • Found a security problem? Tell us at numikeep.com/contact.
© 2026 Numikeep, a Snowpack project.
Privacy policyTermsDevelopersContact us

Coin values shown in Numikeep are CPG retail prices sourced from Greysheet / Collectors Universe, used with attribution. They are estimates for guidance only — actual sale prices vary with market, venue and condition.

Coins pictured are from the National Numismatic Collection at the Smithsonian Institution — public domain.