Developers

API documentation.

Guides, webhooks, SDKs and the full endpoint reference.

Authentication

Bearer tokens

Send your key in the Authorization header on every request. Shepherd stores only a hash of each key.

Authorization: Bearer shep_live_xxxxxxxxxxxxxxxx
Make requests from your server. Keep keys out of browsers and mobile apps.

Scopes

ScopeCan
readEvery GET. Nothing that changes data.
writeRead, plus ingest, classify, scan, entities, rules and the rest of the books.
adminWrite, 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.

Sandbox

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/signup returns a shep_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 /wallets on any address and chain generates realistic activity for it. POST /wallets/{id}/scan adds 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/reset restores the seed data and keeps your key and webhooks. Sandboxes unused for 30 days are deleted. Limit of 10 wallets.
Response · 201
{
  "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" }
}

Get a sandbox key

Webhooks

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.

EventSent whendata
wallet.scan_completedA 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.reclassifiedA 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.ingestedA batch pushed with POST /accounts/{id}/transactions created new rows.account_id, created, duplicates
transactions.classifiedA background classification finished.account_id, classified, needs_review, transactions (up to 200: id, gl_code, confidence, source)
scan.failedA background wallet scan failed.wallet_id, error
period.closedThe books were closed through a date with POST /periods/close.closed_through
pingYou 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 once
hook = shep.webhooks.create(
    "https://api.yourapp.com/shepherd/webhook",
    events=["wallet.scan_completed", "transaction.reclassified"],
)
secret = hook["secret"]   # shown once
const hook = await shep.webhooks.create({
  url: "https://api.yourapp.com/shepherd/webhook",
  events: ["wallet.scan_completed", "transaction.reclassified"],
});
const secret = hook.secret; // shown once

Verify 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 "", 200
import 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 2xx within 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 https and publicly reachable. For local development, use a tunnel.
  • GET /webhooks/{id}/deliveries shows recent attempts with status codes and errors.
SDKs

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);
PythonNode / TypeScript
Packageshepherd-sdk on PyPI@getshepherd/sdk on npm
RequiresPython 3.8+Node 18+
ErrorsShepherdError and subclassesShepherdError and subclasses
Webhooksconstruct_event()constructEvent()
Guide

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.

  1. POST /workspaces with your id for the customer. It is safe to repeat: the same external_id returns the same workspace.
  2. Send your normal requests with the header Shepherd-Workspace: <external_id>. Every endpoint then reads and writes that customer's books only.
  3. 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.

Guide

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.

  1. Create an account with POST /accounts: your external_id, a type of bank, credit_card, stablecoin or ledger, and a currency.
  2. Push transactions with POST /accounts/{id}/transactions, up to 1000 per call. Each item needs your external_id, a date and an amount; add direction, counterparty and category where you have them. The response says how many were created, how many already existed, and lists any rejected items with the reason.
  3. Resend freely. external_id is 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).
  4. Classification runs in the background. The response includes a job_id; poll GET /jobs/{id}, or subscribe to the transactions.classified webhook.

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.

Guide

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.

API reference

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.

NameInTypeDescription
end_user_idquerystringOnly 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.

NameInTypeDescription
addressbodystringWallet address. required
chainbodystringOne of base, ethereum, polygon, arbitrum, tempo, solana. required
end_user_idbodystringYour own identifier for the customer who owns this wallet. Created on first use.
end_user_namebodystringDisplay name for a new end user.
namebodystringLabel for the wallet. Defaults to the address.
lookback_blocksbodyintegerHow 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.

NameInTypeDescription
wallet_idpathintegerWallet id from GET /wallets. required
asyncbodybooleanRun 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).

NameInTypeDescription
end_user_idquerystringOnly 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.

NameInTypeDescription
external_idbodystringYour id for this account. required
namebodystringDisplay name.
typebodystringbank, credit_card, stablecoin, or ledger. Default bank.
currencybodystringUSD, EUR, USDC and so on. Default USD (USDC for stablecoin and ledger).
end_user_idbodystringYour id for the customer who owns it. Created on first use.
end_user_namebodystringDisplay name for a new end user.
entity_idbodyintegerBook this account under an entity.
GET/accounts/{account_id}Fetch one account

Fetch one account.

NameInTypeDescription
account_idpathintegerAccount id. required
GET/accounts/{account_id}/transactionsTransactions for one account

Transactions for one account. Same filters and pagination as GET /transactions.

NameInTypeDescription
account_idpathintegerAccount 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.

NameInTypeDescription
account_idpathintegerAccount id. required
transactionsbodyarrayItems: 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
classifybodybooleanClassify 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.

NameInTypeDescription
account_idpathintegerAccount 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.

NameInTypeDescription
job_idpathstringJob 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.

NameInTypeDescription
wallet_idquerystringFilter to one wallet.
end_user_idquerystringFilter to one end user.
gl_codequerystringFilter to one chart-of-accounts code.
statusquerystringunclassified, classified, or pending_approval.
date_fromquerydateYYYY-MM-DD, inclusive.
date_toquerydateYYYY-MM-DD, inclusive.
updated_sincequerystringISO 8601 timestamp. Returns only transactions created or changed after it, oldest change first, so you can keep a copy in sync.
limitqueryintegerPage size, 1 to 1000. Default 500.
cursorquerystringThe next_cursor from the previous page.
GET/transactions/{transaction_id}Fetch one transaction

Fetch one transaction.

NameInTypeDescription
transaction_idpathintegerTransaction 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.

NameInTypeDescription
transaction_idpathintegerTransaction id. required
gl_codebodystringA code from the chart of accounts, or 'Unclassified'. required
counterpartybodystringCounterparty name to store with the transaction.
reasonbodystringWhy 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.

NameInTypeDescription
tx_id_abodyintegerFirst transaction. required
tx_id_bbodyintegerSecond transaction. required
notebodystringOptional note.
GET/transactions/linksList confirmed links

List confirmed links.

No parameters.

POST/transactions/links/{link_id}/unlinkRemove a link

Remove a link.

NameInTypeDescription
link_idpathintegerLink 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.

NameInTypeDescription
tx_id_abodyintegerFirst transaction. required
tx_id_bbodyintegerSecond 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.

NameInTypeDescription
start_datequerydateYYYY-MM-DD, inclusive.
end_datequerydateYYYY-MM-DD, inclusive.
entity_idqueryintegerLimit to one entity.
wallet_idquerystringLimit to one wallet.
end_user_idquerystringLimit to one end user.
GET/financial-statements/consolidatedIncome statement consolidated across entities

Income statement consolidated across entities.

NameInTypeDescription
start_datequerydateYYYY-MM-DD.
end_datequerydateYYYY-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.

NameInTypeDescription
entity_idqueryintegerLimit to one entity.
as_of_datequerydateYYYY-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.

NameInTypeDescription
as_of_datequerydateYYYY-MM-DD.
period_start_datequerydateYYYY-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.

NameInTypeDescription
namebodystringEntity name. required
legal_namebodystringLegal name.
entity_typebodystringEntity type.
functional_currencybodystringISO currency code.
POST/entities/{entity_id}/updateUpdate an entity

Update an entity.

NameInTypeDescription
entity_idpathintegerEntity id. required
namebodystringEntity name. required
legal_namebodystringLegal name.
entity_typebodystringEntity type.
functional_currencybodystringISO currency code.
clear_functional_currencybodybooleanSet true to remove the functional currency.
POST/entities/{entity_id}/deleteDelete an entity

Delete an entity.

NameInTypeDescription
entity_idpathintegerEntity 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.

NameInTypeDescription
account_namebodystringAccount the balance belongs to. required
account_typebodystringasset, liability, equity, revenue, or expense. required
amountbodynumberBalance. required
as_of_datebodydateYYYY-MM-DD. required
currencybodystringISO code. Default USD.
gl_codebodystringChart-of-accounts code.
POST/opening-balances/{balance_id}/deleteDelete an opening balance

Delete an opening balance.

NameInTypeDescription
balance_idpathintegerOpening 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.

NameInTypeDescription
entry_typebodystringar_invoice or ap_bill. required
amountbodynumberAmount. required
issue_datebodydateYYYY-MM-DD. required
gl_codebodystringChart-of-accounts code.
counterpartybodystringCustomer or vendor.
descriptionbodystringFree text.
due_datebodydateYYYY-MM-DD.
recognition_start_datebodydateStart of recognition.
recognition_monthsbodyintegerMonths to recognize over.
external_sourcebodystringName of the system the entry came from.
external_idbodystringIts 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.

NameInTypeDescription
accrual_idpathintegerAccrual id. required
tx_hashbodystringSettling transaction hash. required
wallet_addressbodystringWallet that sent or received it. required
chainbodystringChain of the transaction. required
matched_datebodydateYYYY-MM-DD. required
POST/accruals/{accrual_id}/deleteDelete an accrual

Delete an accrual.

NameInTypeDescription
accrual_idpathintegerAccrual 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.

NameInTypeDescription
gl_codebodystringCode to apply. required
directionbodystringDirection of the transaction.
chainbodystringChain.
counterpartybodystringCounterparty.
amount_minbodynumberLower bound.
amount_maxbodynumberUpper bound.
entity_idbodyintegerLimit to an entity.
POST/classification-rules/{rule_id}/deleteDelete a rule

Delete a rule.

NameInTypeDescription
rule_idpathintegerRule 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.

NameInTypeDescription
wallet_idbodystringWallet id. required
balancebodynumberBalance 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.

NameInTypeDescription
accountsbodyarrayItems: code and name (required), account_type (asset, liability, equity, revenue, expense, transfer), description, parent_code. required
replacebodybooleanReplace 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.

NameInTypeDescription
throughbodydateA month-end, YYYY-MM-DD, not in the future. required
notebodystringOptional 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.

NameInTypeDescription
tobodydateMonth-end to reopen to, or null.
notebodystringWhy. 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.

NameInTypeDescription
limitqueryinteger1 to 500. Default 100.
cursorquerystringnext_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.

NameInTypeDescription
labelbodystringA name for the key.
scopebodystringread, write or admin. Default write.
expires_in_daysbodyintegerThe key stops working after this many days.
rate_limit_per_minbodyintegerLower 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.

NameInTypeDescription
key_idpathintegerKey id. required
grace_hoursbodyinteger0 to 720. 0 revokes the old key immediately.
POST/api-keys/{key_id}/revokeRevoke a key immediately

Revoke a key immediately.

NameInTypeDescription
key_idpathintegerKey id. required

Workspaces

GET/workspacesList your customers' workspaces

List your customers' workspaces. Platform accounts only.

NameInTypeDescription
limitqueryinteger1 to 500. Default 100.
cursorquerystringnext_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.

NameInTypeDescription
external_idbodystringYour id for the customer. required
namebodystringDisplay name. required
contact_emailbodystringOptional.
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.

NameInTypeDescription
workspace_idpathstringexternal_id or id. required
POST/workspaces/{workspace_id}/updateRename a workspace

Rename a workspace.

NameInTypeDescription
workspace_idpathstringexternal_id or id. required
namebodystringNew name. required
POST/workspaces/{workspace_id}/deletePermanently delete a workspace and all of its data

Permanently delete a workspace and all of its data.

NameInTypeDescription
workspace_idpathstringexternal_id or id. required
confirmbodystringMust 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.

NameInTypeDescription
urlbodystringWhere Shepherd sends events. required
eventsbodyarrayEvent types to receive, or ["*"] for all. Default all.
descriptionbodystringLabel for the endpoint.
POST/webhooks/{webhook_id}/testQueue a ping event to one endpoint

Queue a ping event to one endpoint.

NameInTypeDescription
webhook_idpathintegerWebhook 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.

NameInTypeDescription
webhook_idpathintegerWebhook id. required
POST/webhooks/{webhook_id}/deleteDelete an endpoint

Delete an endpoint.

NameInTypeDescription
webhook_idpathintegerWebhook 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.

NameInTypeDescription
emailbodystringOptional. Where to reach you about the integration.
namebodystringOptional label.
POST/sandbox/resetReset the sandbox to its seed data

Reset the sandbox to its seed data. Sandbox keys only.

No parameters.

Errors

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"
}
StatusMeaning
200 / 201Success. Connecting a wallet returns 201.
400A required field is missing or invalid, for example an unsupported chain or an unknown gl_code. The message says which.
401Missing, malformed, expired or revoked API key.
403The key's scope is too low (insufficient_scope), or the account can't use the feature (not_a_platform_account).
404The wallet, transaction, entity, or other object doesn't exist in your organization.
409The period is closed (period_closed), or a request with the same Idempotency-Key is still running.
413The body is over 8 MB.
422Idempotency-Key reused with a different request.
429Rate limit exceeded. Wait for Retry-After seconds. Sandbox signups are limited per IP.
502A chain data source failed while scanning a wallet. The error body explains; retry the scan.
503Reports are busy (busy) or persistence isn't available. Retry after Retry-After.
Resources

Spec, SDKs and tools

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.

Request a Demo

Tell us a bit about your business and we'll follow up to set up a demo.

Request received

Thanks. We'll be in touch shortly to set up a walkthrough.

Get a sandbox key

Instant. Synthetic data, nothing touches a chain.