Get a free key

Keys API

On this page

For platforms that put a map on many client sites: create a key for each site from your own systems, lock it to that site's domains, give it its own monthly cap, rotate or revoke it, read usage per site, and get signed webhooks. Everything here is also in the OpenAPI spec.

Platform plan. The keys API, API tokens, per-key caps and webhooks come with a platform contract (up to 5,000 keys). On other plans these endpoints answer 403 plan_feature_unavailable; ask about a platform pilot. Usage by key and the CSV in the dashboard are on every plan.
Two kinds of credential. Map keys (ps_…) load maps, images and search on your clients' sites. API tokens (psm_…) manage those keys from your server. A map key never works on the keys API, and an API token never loads a map. Make API tokens in your dashboard under API tokens: up to 10 per account, each with the access it needs.

Access

ScopeAllows
keys:readList and read keys and webhooks
keys:writeCreate, change, rotate and revoke keys; add and delete webhooks
usage:readUsage per key, per day or month, as JSON or CSV

Send the token as Authorization: Bearer psm_…, from your server only. On accounts with two-step sign-in, making or revoking a token asks you to confirm it's you, and every change is emailed to the account owner.

Create a key

curl -X POST https://api.pinsharing.com/v1/keys \
  -H "Authorization: Bearer psm_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: site-48213" \
  -d '{"name": "Smith Realty", "referers": ["smithrealty.com", "*.smithrealty.com"],
       "external_id": "site-48213", "caps": {"tiles": 500000}, "metadata": {"plan": "pro"}}'
HTTP/1.1 201 Created
{
  "id": "key_3f9c2a7d1b8e4c60a5d2",
  "key": "ps_…",                     // shown in this response only
  "prefix": "ps_7Hq2xR",
  "name": "Smith Realty",
  "referers": ["smithrealty.com", "*.smithrealty.com"],
  "external_id": "site-48213",
  "caps": { "static": null, "geocode": null, "tiles": 500000 },
  "metadata": { "plan": "pro" },
  "created_via": "api",
  "created_at": "2026-11-06T15:04:05Z",
  "revoked_at": null,
  "last_used_at": null,
  "usage_this_month": { "static": 0, "geocode": 0, "tiles": 0 }
}
  • referers: the site's host names; *.example.com also covers subdomains. Leave it out only for keys that stay on a server.
  • external_id: your own id for the site (up to 128 characters), unique among the account's live keys. Look keys up by it.
  • caps: optional monthly caps for this key, per product (static, geocode, tiles). A product left out shares the account's cap. A key's cap can't be above the plan's cap and never raises it.
  • metadata: any JSON object up to 4 KB, returned as is.
  • Idempotency-Key: retrying with the same header and body within 24 hours returns the same key (with Idempotent-Replayed: true) instead of making a second one. The same header with a different body is refused with 422.

Find, change, rotate and revoke

# find a site's key, change it, rotate it, revoke it
curl "https://api.pinsharing.com/v1/keys?external_id=site-48213" -H "Authorization: Bearer psm_YOUR_TOKEN"
curl -X PATCH https://api.pinsharing.com/v1/keys/key_3f9c2a7d1b8e4c60a5d2 -H "Authorization: Bearer psm_YOUR_TOKEN" \
  -H "Content-Type: application/json" -d '{"referers": ["smithrealty.com", "smith.homes"], "caps": {"tiles": 750000}}'
curl -X POST https://api.pinsharing.com/v1/keys/key_3f9c2a7d1b8e4c60a5d2/rotate -H "Authorization: Bearer psm_YOUR_TOKEN" \
  -H "Content-Type: application/json" -d '{"grace_seconds": 3600}'
curl -X DELETE https://api.pinsharing.com/v1/keys/key_3f9c2a7d1b8e4c60a5d2 -H "Authorization: Bearer psm_YOUR_TOKEN"
  • GET /v1/keys lists live keys 100 at a time (limit up to 200); pass next_cursor back as cursor for the next page. Add status=all to include revoked keys.
  • PATCH changes only the fields you send. caps replaces all three caps; "external_id": null clears it.
  • Rotate gives the key a new secret and keeps its id, caps and usage. The old secret keeps working for grace_seconds (default 3,600, up to 86,400; 0 ends it at once) until previous_key_valid_until, so you can update the site first.
  • Ids are opaque strings (key_…, whk_…, evt_…); store them as text. Unknown fields in a request body are refused with 400.
  • Revoke stops the key everywhere within about a minute. Keys can be created or rotated at up to 10 a second and 300 a minute per account.

Caps per key

A request is checked against the key's own cap first, then the account's. When a key reaches its cap, that key gets HTTP 429 with "error": "monthly_cap_reached" and "scope": "key" until the first of next month; your other keys keep working. When the account reaches a plan cap, the answer carries "scope": "account". Nothing is ever billed for overage.

Usage

curl "https://api.pinsharing.com/v1/keys/key_3f9c2a7d1b8e4c60a5d2/usage?granularity=day&from=2026-11-01&to=2026-11-30" -H "Authorization: Bearer psm_YOUR_TOKEN"
curl "https://api.pinsharing.com/v1/usage?month=2026-11&group_by=key" -H "Authorization: Bearer psm_YOUR_TOKEN"
curl -o usage.csv "https://api.pinsharing.com/v1/usage?month=2026-11&group_by=key&format=csv" -H "Authorization: Bearer psm_YOUR_TOKEN"

Per key, granularity=day covers up to 400 days (the last 13 months are kept) and granularity=month any range of months. The account report lists every key that was live or used in the month, with an account total row in the CSV. Map tiles served from our CDN count the same as any other request. Per-key counts are written every 30 seconds and as_of says when they last were; a key's usage_this_month is always up to the moment.

You can also download this month's CSV from the dashboard.

Webhooks

curl -X POST https://api.pinsharing.com/v1/webhooks -H "Authorization: Bearer psm_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://hooks.example.com/pinsharing", "events": ["key.usage.80", "key.usage.100", "key.created", "key.revoked"]}'

# check it, see what was sent, resend one, turn it off or back on
curl -X POST https://api.pinsharing.com/v1/webhooks/whk_8b1e0c44f2a95d73e610/test -H "Authorization: Bearer psm_YOUR_TOKEN"
curl "https://api.pinsharing.com/v1/webhooks/whk_8b1e0c44f2a95d73e610/deliveries?status=failed" -H "Authorization: Bearer psm_YOUR_TOKEN"
curl -X POST https://api.pinsharing.com/v1/webhooks/whk_8b1e0c44f2a95d73e610/deliveries/evt_…/redeliver -H "Authorization: Bearer psm_YOUR_TOKEN"
curl -X PATCH https://api.pinsharing.com/v1/webhooks/whk_8b1e0c44f2a95d73e610 -H "Authorization: Bearer psm_YOUR_TOKEN"   -H "Content-Type: application/json" -d '{"enabled": true}'

The answer includes the webhook's signing secret, shown once. Events:

EventWhen
key.usage.80, key.usage.100A key reaches 80% or 100% of its own cap for a product (once per key, product and month)
account.usage.80, account.usage.100The account reaches 80% or 100% of a plan cap (the same moments as the usage emails)
key.created, key.rotated, key.revokedA key is created, rotated or revoked, through the API or the dashboard (data.actor says which)
webhook.testOnly when you call POST /v1/webhooks/{id}/test
POST /pinsharing HTTP/1.1
Content-Type: application/json
PS-Signature: t=1793990000,v1=5d1c…

{
  "id": "evt_…",
  "type": "key.usage.80",
  "created": 1793990000,
  "account": "acct_51c0e2b9a7f34d18c6e0",
  "data": { "key": { "id": "key_3f9c2a7d1b8e4c60a5d2", "name": "Smith Realty", "external_id": "site-48213" },
            "product": "tiles", "level": 80, "used": 400000, "cap": 500000, "month": "2026-11", "resets": "2026-12-01" }
}
  • Up to 5 webhooks per account, on https URLs with a host name (no IP addresses, port 443) that resolves to public addresses only. Every delivery connects to the address it checked. Redirects aren't followed.
  • Answer with any 2xx within 10 seconds. Anything else is retried with growing gaps (30 seconds up to an hour) for 24 hours, then marked failed. At most two deliveries to one webhook are in flight at a time.
  • A webhook that answers 410 Gone, or fails every delivery for 3 days, is turned off and the account owner gets an email. Turn it back on with PATCH and {"enabled": true}.
  • GET /v1/webhooks shows each webhook's status and latest delivery; /deliveries lists the last 30 days, and /redeliver sends one again with the same event id.
  • Deliveries can arrive more than once or out of order: use the event id to ignore repeats.

Check the signature

PS-Signature is t=<unix seconds>,v1=<hex HMAC-SHA256>, computed over <t>.<raw body> with the webhook's secret. Compare in constant time and reject old timestamps.

Node.js

import crypto from "node:crypto";

// rawBody: the request body exactly as received (a string or Buffer), before any JSON parsing.
export function verifyPinSharing(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(String(header || "").split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const want = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const got = Buffer.from(parts.v1 || "", "hex");
  return got.length === want.length && crypto.timingSafeEqual(got, want);
}

Python

import hashlib, hmac, time

def verify_pinsharing(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    """raw_body: the request body exactly as received, before any JSON parsing."""
    parts = dict(p.split("=", 1) for p in (header or "").split(",") if "=" in p)
    try:
        t = int(parts.get("t", "0"))
    except ValueError:
        return False
    if abs(time.time() - t) > tolerance:
        return False
    want = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, parts.get("v1", ""))

Errors

StatuserrorMeaning
401missing_token, invalid_tokenNo API token, a map key instead of a token, or a revoked token
403insufficient_scope, plan_feature_unavailableThe token lacks the scope this call needs, or the account isn't on the platform plan
404not_foundNo such key or webhook on this account (or it's already revoked)
409key_limit_reached, external_id_taken, webhook_limit_reached, idempotency_in_progressThe plan's key limit (Free 1, trial 3, paid plans 20, platform contracts 5,000), a duplicate external_id, 5 webhooks already, or the same Idempotency-Key still running
422idempotency_key_reusedAn Idempotency-Key reused with a different body
429slow_downOver the key creation rate; retry after a second
↑ Contents