Bearer tokens
Send your key in the Authorization header on every request. Shepherd stores only a hash of each key.
Authorization: Bearer shep_live_xxxxxxxxxxxxxxxxScopes
| Scope | Can |
|---|---|
read | Every GET. Nothing that changes data. |
write | Read, plus ingest, classify, scan, entities, rules and the rest of the books. |
admin | Write, plus managing keys, workspaces, the chart replacement, and closing periods. |
Create a key per integration with POST /api-keys, give it the least scope it needs, and set expires_in_days if it is temporary. POST /api-keys/{id}/rotate issues a replacement and keeps the old key working for a grace period (24 hours by default), so you can roll a key without downtime.
Rate limits
Each key gets 600 requests a minute (120 in the sandbox), or the limit you set on it. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Past the limit you get 429 with Retry-After. Reports (income statement, balance sheet) run a few at a time; if they are all busy you get 503 with Retry-After.
Safe retries
Add an Idempotency-Key header (any unique string, up to 255 characters) to a POST. Sending the same key and body again within 24 hours returns the first response, with Idempotent-Replayed: true, and does nothing twice. The same key with a different body returns 422. Every response also carries an X-Request-Id to quote when you contact us.
Build before you have a live key
A sandbox is a private, throwaway copy of the API with synthetic data. Every endpoint behaves as it does live, except that nothing touches a chain.
- Instant.
POST /sandbox/signupreturns ashep_test_key and seeds a demo customer (cust_demo) with a wallet, 40 classified transactions over about 90 days, and a couple left for review. - Connect any address.
POST /walletson any address and chain generates realistic activity for it.POST /wallets/{id}/scanadds a few new transactions each time, so polling and webhooks have something to show. - Same everything else. Classification, entities, statements, opening balances, accruals, rules, reconciliation and webhooks all work as documented.
- Reset any time.
POST /sandbox/resetrestores the seed data and keeps your key and webhooks. Sandboxes unused for 30 days are deleted. Limit of 10 wallets.
{
"api_key": "shep_test_...",
"livemode": false,
"base_url": "https://app.getshepherd.co/api/v1",
"seeded": { "end_user_id": "cust_demo", "wallet_id": 1, "address": "0x5a4d...c0de", "chain": "base" }
}Know when the books change
Register an HTTPS endpoint and Shepherd will POST a signed JSON event when something happens, so you don't have to poll.
| Event | Sent when | data |
|---|---|---|
wallet.scan_completed | A wallet was connected or scanned, by you or by a schedule. | wallet_id, address, chain, end_user_id, transactions_saved, transactions_classified, transactions_needing_review |
transaction.reclassified | A classified transaction was moved to a different GL code, from the API or the app. | transaction_id, to_gl_code, reason, reclassification_id, approval_status |
transactions.ingested | A batch pushed with POST /accounts/{id}/transactions created new rows. | account_id, created, duplicates |
transactions.classified | A background classification finished. | account_id, classified, needs_review, transactions (up to 200: id, gl_code, confidence, source) |
scan.failed | A background wallet scan failed. | wallet_id, error |
period.closed | The books were closed through a date with POST /periods/close. | closed_through |
ping | You called POST /webhooks/{id}/test. | message |
Event shape
{
"id": "evt_55881f55b8e7844ada399eac",
"type": "wallet.scan_completed",
"created": 1792380604,
"livemode": true,
"data": {
"wallet_id": 17,
"address": "0xYourWalletAddress",
"chain": "base",
"end_user_id": "cust_4821",
"transactions_saved": 12,
"transactions_classified": 10,
"transactions_needing_review": 2,
"triggered_by": "api"
}
}livemode is false for sandbox events. Use id to deduplicate: delivery is at least once, and order isn't guaranteed.
Register an endpoint
curl -X POST https://app.getshepherd.co/api/v1/webhooks \
-H "Authorization: Bearer $SHEPHERD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://api.yourapp.com/shepherd/webhook",
"events": ["wallet.scan_completed", "transaction.reclassified"]}'
# => { "id": 3, "secret": "whsec_...", ... } store the secret, it is shown oncehook = shep.webhooks.create(
"https://api.yourapp.com/shepherd/webhook",
events=["wallet.scan_completed", "transaction.reclassified"],
)
secret = hook["secret"] # shown onceconst hook = await shep.webhooks.create({
url: "https://api.yourapp.com/shepherd/webhook",
events: ["wallet.scan_completed", "transaction.reclassified"],
});
const secret = hook.secret; // shown onceVerify the signature
Every request carries Shepherd-Signature: t=<unix time>,v1=<hex>. The signature is HMAC-SHA256, keyed with your endpoint secret, over the timestamp, a dot, and the raw request body. Verify against the raw bytes, not re-serialized JSON, and reject timestamps more than five minutes old.
from flask import Flask, request
from shepherd import construct_event, SignatureVerificationError
app = Flask(__name__)
@app.post("/shepherd/webhook")
def shepherd_webhook():
try:
event = construct_event(request.get_data(), request.headers["Shepherd-Signature"], WEBHOOK_SECRET)
except SignatureVerificationError:
return "", 400
if event["type"] == "wallet.scan_completed":
refresh_books(event["data"]["end_user_id"])
return "", 200import express from "express";
import { constructEvent } from "@getshepherd/sdk";
const app = express();
app.post("/shepherd/webhook", express.raw({ type: "application/json" }), (req, res) => {
try {
const event = constructEvent(req.body, req.header("Shepherd-Signature"), process.env.WEBHOOK_SECRET);
if (event.type === "wallet.scan_completed") refreshBooks(event.data.end_user_id);
res.sendStatus(200);
} catch {
res.sendStatus(400);
}
});# The signature is HMAC-SHA256 over "{t}.{raw body}", hex-encoded.
# Header: Shepherd-Signature: t=1792380604,v1=5257a869e7ec...
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(parts["t"])) <= tolerance
return fresh and hmac.compare_digest(expected, parts["v1"])Delivery and retries
- Respond with any
2xxwithin 10 seconds. Anything else, or a timeout, is retried. - Up to 7 attempts: immediately, then after 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours and 24 hours. After that the delivery is marked
failed. - Redirects aren't followed. URLs must be
httpsand publicly reachable. For local development, use a tunnel. GET /webhooks/{id}/deliveriesshows recent attempts with status codes and errors.
Official Python and Node clients
Typed, with automatic retries on reads, error classes for each status, createSandbox(), and webhook verification built in. Node is zero-dependency. For any other language, generate a client from the OpenAPI spec.
# pip install shepherd-sdk (Python 3.8+)
from shepherd import Shepherd
shep = Shepherd("shep_live_...")
wallet = shep.wallets.connect("0x...", "base", end_user_id="cust_4821")
pnl = shep.statements.income(start_date="2026-09-01", end_user_id="cust_4821")
for hook in shep.webhooks.list():
print(hook["url"], hook["events"])// npm install @getshepherd/sdk (Node 18+, zero dependencies, ships types)
import { Shepherd } from "@getshepherd/sdk";
const shep = new Shepherd(process.env.SHEPHERD_API_KEY);
const wallet = await shep.wallets.connect({ address: "0x...", chain: "base", end_user_id: "cust_4821" });
const pnl = await shep.statements.income({ start_date: "2026-09-01", end_user_id: "cust_4821" });
for (const hook of await shep.webhooks.list()) console.log(hook.url, hook.events);| Python | Node / TypeScript | |
|---|---|---|
| Package | shepherd-sdk on PyPI | @getshepherd/sdk on npm |
| Requires | Python 3.8+ | Node 18+ |
| Errors | ShepherdError and subclasses | ShepherdError and subclasses |
| Webhooks | construct_event() | constructEvent() |
Serving multiple customers
Platforms usually need to keep each customer's books apart. There are two building blocks, and they combine.
- End users. Tag wallets with your own
end_user_id. The wallets, transactions list, and income statement can be filtered to one end user. Lightweight, and a good fit when you mostly need per-customer views. - Entities. Create an entity per customer for separate sets of books, then read each one, or a consolidated view across all of them, from the statement endpoints.
Most platforms use both: an end user tag for lightweight per-customer views, and an entity where a customer needs its own books.
Workspaces: one set of books per customer
If you embed Shepherd for your own customers, give each one a workspace. A workspace is a fully separate organization: its own accounts, transactions, chart of accounts, rules, statements and period locks. Nothing is shared between workspaces.
POST /workspaceswith your id for the customer. It is safe to repeat: the sameexternal_idreturns the same workspace.- Send your normal requests with the header
Shepherd-Workspace: <external_id>. Every endpoint then reads and writes that customer's books only. - Without the header you are in your own organization. A workspace can only be reached with a key from the platform account that created it.
Workspaces are on for every API account (sandbox keys can try them, up to 10). POST /workspaces/{id}/delete permanently removes a workspace and its data; it requires confirm set to the external_id.
Push your own transactions
If you already hold the customer's money movements, such as a bank ledger or custodial stablecoin accounts, send them to Shepherd instead of connecting a wallet. Shepherd books and classifies them the same way.
- Create an account with
POST /accounts: yourexternal_id, atypeofbank,credit_card,stablecoinorledger, and acurrency. - Push transactions with
POST /accounts/{id}/transactions, up to 1000 per call. Each item needs yourexternal_id, adateand anamount; adddirection,counterpartyandcategorywhere you have them. The response says how many were created, how many already existed, and lists any rejected items with the reason. - Resend freely.
external_idis the key: the same transaction twice is stored once, and a corrected amount or date updates it (unless that date falls in a closed period, in which case the stored row is left as it was and flagged for review). - Classification runs in the background. The response includes a
job_id; pollGET /jobs/{id}, or subscribe to thetransactions.classifiedwebhook.
Foreign currency amounts are converted to the books' functional currency at the rate on the transaction date. A transaction in a currency with no available rate is stored and held out of the statements until one exists, and counted in the statements' unpriced total.
Pagination and syncing
GET /transactions, /audit-log and /workspaces return a page plus has_more and next_cursor. Pass next_cursor back as cursor until has_more is false. limit is 1 to 1000.
To keep a copy of the books in your own database, call GET /transactions?updated_since=<timestamp>. Results come oldest change first, so you can store the timestamp of the last row you processed and ask again from there. Subscribe to webhooks to know when to ask.
Endpoints
Every path below is relative to https://app.getshepherd.co/api/v1. Mutations use POST, including deletes. Long lists are paginated (see Pagination and syncing); other lists are short and unpaginated.
Wallets
GET/walletsList connected wallets
List connected wallets.
| Name | In | Type | Description |
|---|---|---|---|
end_user_id | query | string | Only wallets tagged with this end user. |
POST/walletsConnect a wallet, pull its history, and auto-classify what it finds
Connect a wallet, pull its history, and auto-classify what it finds. Returns 201.
| Name | In | Type | Description |
|---|---|---|---|
address | body | string | Wallet address. required |
chain | body | string | One of base, ethereum, polygon, arbitrum, tempo, solana. required |
end_user_id | body | string | Your own identifier for the customer who owns this wallet. Created on first use. |
end_user_name | body | string | Display name for a new end user. |
name | body | string | Label for the wallet. Defaults to the address. |
lookback_blocks | body | integer | How far back to scan on EVM chains. Default 2000. |
POST/wallets/{wallet_id}/scanRescan a wallet for new activity
Rescan a wallet for new activity. Add async: true to return 202 with a job_id right away instead of waiting.
| Name | In | Type | Description |
|---|---|---|---|
wallet_id | path | integer | Wallet id from GET /wallets. required |
async | body | boolean | Run in the background and return a job (GET /jobs/{job_id}). |
Accounts
GET/accountsList accounts you push transactions to (bank accounts, cards, and custodial stablecoin accounts)
List accounts you push transactions to (bank accounts, cards, and custodial stablecoin accounts).
| Name | In | Type | Description |
|---|---|---|---|
end_user_id | query | string | Only accounts for this end user. |
POST/accountsCreate an account, or return the existing one with the same external_id
Create an account, or return the existing one with the same external_id. Returns 201 when created, 200 otherwise.
| Name | In | Type | Description |
|---|---|---|---|
external_id | body | string | Your id for this account. required |
name | body | string | Display name. |
type | body | string | bank, credit_card, stablecoin, or ledger. Default bank. |
currency | body | string | USD, EUR, USDC and so on. Default USD (USDC for stablecoin and ledger). |
end_user_id | body | string | Your id for the customer who owns it. Created on first use. |
end_user_name | body | string | Display name for a new end user. |
entity_id | body | integer | Book this account under an entity. |
GET/accounts/{account_id}Fetch one account
Fetch one account.
| Name | In | Type | Description |
|---|---|---|---|
account_id | path | integer | Account id. required |
GET/accounts/{account_id}/transactionsTransactions for one account
Transactions for one account. Same filters and pagination as GET /transactions.
| Name | In | Type | Description |
|---|---|---|---|
account_id | path | integer | Account id. required |
POST/accounts/{account_id}/transactionsPush up to 1000 transactions
Push up to 1000 transactions. Each is keyed by your external_id, so sending one again changes nothing, and sending it with a corrected amount or date updates it. Returns 200, or 207 when some items were rejected (listed with their index and reason). By default new transactions are then classified in the background.
| Name | In | Type | Description |
|---|---|---|---|
account_id | path | integer | Account id. required |
transactions | body | array | Items: external_id (required), date YYYY-MM-DD (required), amount (required), direction inflow or outflow (a negative amount means outflow if omitted), counterparty, currency or asset, category, merchant_id, pending. required |
classify | body | boolean | Classify new transactions in the background. Default true. |
POST/accounts/{account_id}/classifyClassify the account's unclassified transactions in the background
Classify the account's unclassified transactions in the background. Returns 202 with a job_id.
| Name | In | Type | Description |
|---|---|---|---|
account_id | path | integer | Account id. required |
Jobs
GET/jobsYour 50 most recent background jobs
Your 50 most recent background jobs.
No parameters.
GET/jobs/{job_id}Status of a background job: queued, running, succeeded or failed, with its result or error
Status of a background job: queued, running, succeeded or failed, with its result or error.
| Name | In | Type | Description |
|---|---|---|---|
job_id | path | string | Job id from a 202 response. required |
Transactions
GET/transactionsList transactions with their classification, newest first
List transactions with their classification, newest first. Paginated: pass next_cursor back as cursor until has_more is false.
| Name | In | Type | Description |
|---|---|---|---|
wallet_id | query | string | Filter to one wallet. |
end_user_id | query | string | Filter to one end user. |
gl_code | query | string | Filter to one chart-of-accounts code. |
status | query | string | unclassified, classified, or pending_approval. |
date_from | query | date | YYYY-MM-DD, inclusive. |
date_to | query | date | YYYY-MM-DD, inclusive. |
updated_since | query | string | ISO 8601 timestamp. Returns only transactions created or changed after it, oldest change first, so you can keep a copy in sync. |
limit | query | integer | Page size, 1 to 1000. Default 500. |
cursor | query | string | The next_cursor from the previous page. |
GET/transactions/{transaction_id}Fetch one transaction
Fetch one transaction.
| Name | In | Type | Description |
|---|---|---|---|
transaction_id | path | integer | Transaction id. required |
POST/transactions/{transaction_id}/classifySet or change a transaction's GL code
Set or change a transaction's GL code. Changes to an already-classified transaction are recorded as reclassifications.
| Name | In | Type | Description |
|---|---|---|---|
transaction_id | path | integer | Transaction id. required |
gl_code | body | string | A code from the chart of accounts, or 'Unclassified'. required |
counterparty | body | string | Counterparty name to store with the transaction. |
reason | body | string | Why it was reclassified. Kept in the audit trail. |
GET/transactions/link-suggestionsSuggested pairs of transactions (for example a bank leg and a wallet leg of one conversion) for you to confirm
Suggested pairs of transactions (for example a bank leg and a wallet leg of one conversion) for you to confirm.
No parameters.
POST/transactions/linkLink two transactions as the two legs of one event
Link two transactions as the two legs of one event.
| Name | In | Type | Description |
|---|---|---|---|
tx_id_a | body | integer | First transaction. required |
tx_id_b | body | integer | Second transaction. required |
note | body | string | Optional note. |
GET/transactions/linksList confirmed links
List confirmed links.
No parameters.
POST/transactions/links/{link_id}/unlinkRemove a link
Remove a link.
| Name | In | Type | Description |
|---|---|---|---|
link_id | path | integer | Link id from GET /transactions/links. required |
POST/transactions/link-suggestions/dismissDismiss a suggested pair so it stops appearing
Dismiss a suggested pair so it stops appearing.
| Name | In | Type | Description |
|---|---|---|---|
tx_id_a | body | integer | First transaction. required |
tx_id_b | body | integer | Second transaction. required |
Financial statements
GET/financial-statementsIncome statement totals: revenue and expense by account, fair value and FX gains, net income, plus counts of unclassified and unpriced activity
Income statement totals: revenue and expense by account, fair value and FX gains, net income, plus counts of unclassified and unpriced activity.
| Name | In | Type | Description |
|---|---|---|---|
start_date | query | date | YYYY-MM-DD, inclusive. |
end_date | query | date | YYYY-MM-DD, inclusive. |
entity_id | query | integer | Limit to one entity. |
wallet_id | query | string | Limit to one wallet. |
end_user_id | query | string | Limit to one end user. |
GET/financial-statements/consolidatedIncome statement consolidated across entities
Income statement consolidated across entities.
| Name | In | Type | Description |
|---|---|---|---|
start_date | query | date | YYYY-MM-DD. |
end_date | query | date | YYYY-MM-DD. |
GET/balance-sheetBalance sheet for one entity, or the whole organization if entity_id is omitted
Balance sheet for one entity, or the whole organization if entity_id is omitted.
| Name | In | Type | Description |
|---|---|---|---|
entity_id | query | integer | Limit to one entity. |
as_of_date | query | date | YYYY-MM-DD. Defaults to today. |
GET/balance-sheet/consolidatedConsolidated balance sheet, including the cumulative translation adjustment for entities with different functional currencies
Consolidated balance sheet, including the cumulative translation adjustment for entities with different functional currencies.
| Name | In | Type | Description |
|---|---|---|---|
as_of_date | query | date | YYYY-MM-DD. |
period_start_date | query | date | YYYY-MM-DD. |
Entities
GET/entitiesList entities (separate sets of books)
List entities (separate sets of books).
No parameters.
POST/entitiesCreate an entity
Create an entity.
| Name | In | Type | Description |
|---|---|---|---|
name | body | string | Entity name. required |
legal_name | body | string | Legal name. |
entity_type | body | string | Entity type. |
functional_currency | body | string | ISO currency code. |
POST/entities/{entity_id}/updateUpdate an entity
Update an entity.
| Name | In | Type | Description |
|---|---|---|---|
entity_id | path | integer | Entity id. required |
name | body | string | Entity name. required |
legal_name | body | string | Legal name. |
entity_type | body | string | Entity type. |
functional_currency | body | string | ISO currency code. |
clear_functional_currency | body | boolean | Set true to remove the functional currency. |
POST/entities/{entity_id}/deleteDelete an entity
Delete an entity.
| Name | In | Type | Description |
|---|---|---|---|
entity_id | path | integer | Entity id. required |
Opening balances
GET/opening-balancesList opening balances
List opening balances.
No parameters.
POST/opening-balancesRecord a balance carried in from before Shepherd
Record a balance carried in from before Shepherd.
| Name | In | Type | Description |
|---|---|---|---|
account_name | body | string | Account the balance belongs to. required |
account_type | body | string | asset, liability, equity, revenue, or expense. required |
amount | body | number | Balance. required |
as_of_date | body | date | YYYY-MM-DD. required |
currency | body | string | ISO code. Default USD. |
gl_code | body | string | Chart-of-accounts code. |
POST/opening-balances/{balance_id}/deleteDelete an opening balance
Delete an opening balance.
| Name | In | Type | Description |
|---|---|---|---|
balance_id | path | integer | Opening balance id. required |
Accruals
GET/accrualsList AR invoices and AP bills, with any possible duplicates flagged
List AR invoices and AP bills, with any possible duplicates flagged.
No parameters.
POST/accrualsRecord an AR invoice or AP bill, optionally recognized over several months
Record an AR invoice or AP bill, optionally recognized over several months.
| Name | In | Type | Description |
|---|---|---|---|
entry_type | body | string | ar_invoice or ap_bill. required |
amount | body | number | Amount. required |
issue_date | body | date | YYYY-MM-DD. required |
gl_code | body | string | Chart-of-accounts code. |
counterparty | body | string | Customer or vendor. |
description | body | string | Free text. |
due_date | body | date | YYYY-MM-DD. |
recognition_start_date | body | date | Start of recognition. |
recognition_months | body | integer | Months to recognize over. |
external_source | body | string | Name of the system the entry came from. |
external_id | body | string | Its id in that system. |
POST/accruals/{accrual_id}/matchMatch an invoice or bill to the on-chain payment that settled it
Match an invoice or bill to the on-chain payment that settled it.
| Name | In | Type | Description |
|---|---|---|---|
accrual_id | path | integer | Accrual id. required |
tx_hash | body | string | Settling transaction hash. required |
wallet_address | body | string | Wallet that sent or received it. required |
chain | body | string | Chain of the transaction. required |
matched_date | body | date | YYYY-MM-DD. required |
POST/accruals/{accrual_id}/deleteDelete an accrual
Delete an accrual.
| Name | In | Type | Description |
|---|---|---|---|
accrual_id | path | integer | Accrual id. required |
Classification rules
GET/classification-rulesList rules that classify matching activity automatically
List rules that classify matching activity automatically.
No parameters.
POST/classification-rulesCreate a rule
Create a rule. A transaction matching every field you set gets the GL code.
| Name | In | Type | Description |
|---|---|---|---|
gl_code | body | string | Code to apply. required |
direction | body | string | Direction of the transaction. |
chain | body | string | Chain. |
counterparty | body | string | Counterparty. |
amount_min | body | number | Lower bound. |
amount_max | body | number | Upper bound. |
entity_id | body | integer | Limit to an entity. |
POST/classification-rules/{rule_id}/deleteDelete a rule
Delete a rule.
| Name | In | Type | Description |
|---|---|---|---|
rule_id | path | integer | Rule id. required |
Reconciliation
GET/reconciliationCompare each wallet's balance computed from transactions against its live on-chain balance
Compare each wallet's balance computed from transactions against its live on-chain balance.
No parameters.
POST/reconciliation/baselineSet a baseline balance for a wallet, so later checks measure activity from that point
Set a baseline balance for a wallet, so later checks measure activity from that point.
| Name | In | Type | Description |
|---|---|---|---|
wallet_id | body | string | Wallet id. required |
balance | body | number | Balance at baseline. required |
Chart of accounts
GET/chart-of-accountsThe chart of accounts
The chart of accounts.
No parameters.
POST/chart-of-accountsAdd accounts, or with replace: true (admin key) replace the whole chart
Add accounts, or with replace: true (admin key) replace the whole chart. Existing codes are never overwritten by an add.
| Name | In | Type | Description |
|---|---|---|---|
accounts | body | array | Items: code and name (required), account_type (asset, liability, equity, revenue, expense, transfer), description, parent_code. required |
replace | body | boolean | Replace the whole chart. Needs an admin key. |
Periods
GET/periodsThe closed-through date, close history, and late-arriving activity
The closed-through date, close history, and late-arriving activity.
No parameters.
POST/periods/closeClose the books through a month-end
Close the books through a month-end. Activity dated on or before it can no longer change; late arrivals are flagged instead. Needs an admin key.
| Name | In | Type | Description |
|---|---|---|---|
through | body | date | A month-end, YYYY-MM-DD, not in the future. required |
note | body | string | Optional note kept in the audit trail. |
POST/periods/reopenReopen to an earlier month-end, or null to reopen everything
Reopen to an earlier month-end, or null to reopen everything. A note is required. Needs an admin key.
| Name | In | Type | Description |
|---|---|---|---|
to | body | date | Month-end to reopen to, or null. |
note | body | string | Why. Kept in the audit trail. required |
Audit log
GET/audit-logWhat changed and who changed it, newest first
What changed and who changed it, newest first.
| Name | In | Type | Description |
|---|---|---|---|
limit | query | integer | 1 to 500. Default 100. |
cursor | query | string | next_cursor from the previous page. |
API keys
GET/api-keysList your keys: label, prefix, scope, last use, expiry
List your keys: label, prefix, scope, last use, expiry. Never the secret. Needs an admin key.
No parameters.
POST/api-keysCreate a key
Create a key. The secret is shown once. Returns 201. Needs an admin key.
| Name | In | Type | Description |
|---|---|---|---|
label | body | string | A name for the key. |
scope | body | string | read, write or admin. Default write. |
expires_in_days | body | integer | The key stops working after this many days. |
rate_limit_per_min | body | integer | Lower or raise this key's request limit. |
POST/api-keys/{key_id}/rotateIssue a replacement with the same scope and limits
Issue a replacement with the same scope and limits. The old key keeps working for grace_hours (default 24). Returns 201.
| Name | In | Type | Description |
|---|---|---|---|
key_id | path | integer | Key id. required |
grace_hours | body | integer | 0 to 720. 0 revokes the old key immediately. |
POST/api-keys/{key_id}/revokeRevoke a key immediately
Revoke a key immediately.
| Name | In | Type | Description |
|---|---|---|---|
key_id | path | integer | Key id. required |
Workspaces
GET/workspacesList your customers' workspaces
List your customers' workspaces. Platform accounts only.
| Name | In | Type | Description |
|---|---|---|---|
limit | query | integer | 1 to 500. Default 100. |
cursor | query | string | next_cursor from the previous page. |
POST/workspacesCreate a workspace for one of your customers, or return the existing one with the same external_id
Create a workspace for one of your customers, or return the existing one with the same external_id. Returns 201 when created.
| Name | In | Type | Description |
|---|---|---|---|
external_id | body | string | Your id for the customer. required |
name | body | string | Display name. required |
contact_email | body | string | Optional. |
GET/workspaces/{workspace_id}Fetch a workspace by its external_id or its Shepherd id
Fetch a workspace by its external_id or its Shepherd id.
| Name | In | Type | Description |
|---|---|---|---|
workspace_id | path | string | external_id or id. required |
POST/workspaces/{workspace_id}/updateRename a workspace
Rename a workspace.
| Name | In | Type | Description |
|---|---|---|---|
workspace_id | path | string | external_id or id. required |
name | body | string | New name. required |
POST/workspaces/{workspace_id}/deletePermanently delete a workspace and all of its data
Permanently delete a workspace and all of its data.
| Name | In | Type | Description |
|---|---|---|---|
workspace_id | path | string | external_id or id. required |
confirm | body | string | Must equal the workspace's external_id. required |
Webhooks
GET/webhooksList your webhook endpoints and the event types you can subscribe to
List your webhook endpoints and the event types you can subscribe to.
No parameters.
POST/webhooksRegister an endpoint
Register an endpoint. The response includes its signing secret, shown once. The URL must be https and publicly reachable. Returns 201.
| Name | In | Type | Description |
|---|---|---|---|
url | body | string | Where Shepherd sends events. required |
events | body | array | Event types to receive, or ["*"] for all. Default all. |
description | body | string | Label for the endpoint. |
POST/webhooks/{webhook_id}/testQueue a ping event to one endpoint
Queue a ping event to one endpoint.
| Name | In | Type | Description |
|---|---|---|---|
webhook_id | path | integer | Webhook id. required |
GET/webhooks/{webhook_id}/deliveriesThe 50 most recent deliveries to an endpoint, with status, attempts and last error
The 50 most recent deliveries to an endpoint, with status, attempts and last error.
| Name | In | Type | Description |
|---|---|---|---|
webhook_id | path | integer | Webhook id. required |
POST/webhooks/{webhook_id}/deleteDelete an endpoint
Delete an endpoint.
| Name | In | Type | Description |
|---|---|---|---|
webhook_id | path | integer | Webhook id. required |
Sandbox
POST/sandbox/signupCreate a sandbox and get a shep_test_ key instantly
Create a sandbox and get a shep_test_ key instantly. No authentication. Returns 201.
| Name | In | Type | Description |
|---|---|---|---|
email | body | string | Optional. Where to reach you about the integration. |
name | body | string | Optional label. |
POST/sandbox/resetReset the sandbox to its seed data
Reset the sandbox to its seed data. Sandbox keys only.
No parameters.
Status codes
Errors return a non-2xx status and a JSON body with code (stable, safe to branch on), message (for people), request_id, and error (the same text as message, kept for older clients). Malformed JSON returns 400 invalid_json.
{
"error": "chain must be one of: base, ethereum, polygon, arbitrum, tempo, solana",
"code": "invalid_request",
"message": "chain must be one of: base, ethereum, polygon, arbitrum, tempo, solana",
"request_id": "req_9c1f0a7e4b2d6a83c5e1"
}| Status | Meaning |
|---|---|
200 / 201 | Success. Connecting a wallet returns 201. |
400 | A required field is missing or invalid, for example an unsupported chain or an unknown gl_code. The message says which. |
401 | Missing, malformed, expired or revoked API key. |
403 | The key's scope is too low (insufficient_scope), or the account can't use the feature (not_a_platform_account). |
404 | The wallet, transaction, entity, or other object doesn't exist in your organization. |
409 | The period is closed (period_closed), or a request with the same Idempotency-Key is still running. |
413 | The body is over 8 MB. |
422 | Idempotency-Key reused with a different request. |
429 | Rate limit exceeded. Wait for Retry-After seconds. Sandbox signups are limited per IP. |
502 | A chain data source failed while scanning a wallet. The error body explains; retry the scan. |
503 | Reports are busy (busy) or persistence isn't available. Retry after Retry-After. |
Spec, SDKs and tools
pip install shepherd-sdkView →
Node SDKnpm install @getshepherd/sdkView →
SandboxAn instant test key with synthetic data.View →
WebhooksSigned events, retries, and a delivery log.View →
QuickstartConnect a wallet and read a P&L in four calls.View →
Go live
Live keys are issued to partners after a short onboarding. Request one and the Shepherd team will set up access and support your integration.