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 } }; withcoins:read, alsocoins: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.
- 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. - They come back to your
redirect_uriwithcodeand yourstate(check it matches), or witherror=access_denied. - 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 tokensnkat_, refresh tokensnkrt_— 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.