Manage your digital business cards programmatically, pull scan analytics into your own systems, and receive real-time webhook notifications for card, scan, and lead events.
Go to Settings → Developer and create a token with the scopes you need. Copy it immediately — it is shown only once.
Call any endpoint with the token in the Authorization header. Try it in the playground below.
curl https://shepcard.com/api/v1/cards \ -H "Authorization: Bearer scp_YOUR_TOKEN_HERE"
All endpoints require a Bearer token in the Authorization header. Tokens start with scp_ followed by 48 hex characters and can carry any combination of three scopes:
| Scope | Grants |
|---|---|
| read | List, get, and view cards |
| write | Create, update, and delete cards; register webhooks |
| analytics | Fetch scan analytics |
ShepCard stores only a SHA-256 hash of your token. The plaintext is shown exactly once at creation and can never be recovered — delete and recreate if you lose it.
Deleting a token in Settings stops it working immediately. Plan downgrades are also enforced per request, so a lapsed Enterprise subscription disables tokens right away.
/cardsreadReturns all of your cards, newest first. Pass ?archived=true to include archived cards (excluded by default).
curl "https://shepcard.com/api/v1/cards" \ -H "Authorization: Bearer scp_YOUR_TOKEN_HERE"
{
"cards": [
{
"id": "cmsf2y1k20001abcde1234567",
"name": "Demo Card",
"slug": "demo",
"published": true,
"archived": false,
"theme": { "palette": "ocean", "colors": { "primary": "#0ea5e9" } },
"background": null,
"sections": [],
"seoTitle": null,
"seoDescription": null,
"viewCount": 128,
"teamId": null,
"createdAt": "2026-07-20T09:14:22.000Z",
"updatedAt": "2026-08-02T16:40:05.000Z"
}
]
}/cardswriteCreates a card and fires the card.created webhook. Your plan's card limit applies here too — exceeding it returns 403.
| Field | Type | Required |
|---|---|---|
| name | string | yes |
| slug | string (custom URL; defaults to a slug from name) | no |
| published | boolean | no |
| theme | object (palette + colors) | no |
| background | string | null (image URL) | no |
| sections | array (ordered card sections) | no |
curl -X POST https://shepcard.com/api/v1/cards \
-H "Authorization: Bearer scp_YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"name": "Ada Lovelace — Card",
"published": true,
"theme": { "palette": "ocean" },
"sections": [
{ "id": "sec_1", "type": "profile", "data": { "name": "Ada Lovelace", "title": "Mathematician" } },
{ "id": "sec_2", "type": "contact", "data": { "email": "[email protected]", "phone": "+233 20 000 0000" } }
]
}'{
"card": {
"id": "cmsf3aaaa0001abcd12345678",
"name": "Ada Lovelace — Card",
"slug": "ada-lovelace-card",
"published": true,
"archived": false,
"theme": { "palette": "ocean" },
"background": null,
"sections": [
{ "id": "sec_1", "type": "profile", "data": { "name": "Ada Lovelace", "title": "Mathematician" } },
{ "id": "sec_2", "type": "contact", "data": { "email": "[email protected]", "phone": "+233 20 000 0000" } }
],
"seoTitle": null,
"seoDescription": null,
"viewCount": 0,
"teamId": null,
"createdAt": "2026-08-04T10:00:00.000Z",
"updatedAt": "2026-08-04T10:00:00.000Z"
}
}/cards/{cardId}readReturns one card. A card that does not exist — or belongs to another account — returns 404, never 403.
curl https://shepcard.com/api/v1/cards/cmsf3aaaa0001abcd12345678 \ -H "Authorization: Bearer scp_YOUR_TOKEN_HERE"
/cards/{cardId}writePartial update — send only the fields you want to change. All create fields are accepted, plus seoTitle and seoDescription. Changing slug to one already in use returns 409. Fires card.updated.
curl -X PUT https://shepcard.com/api/v1/cards/cmsf3aaaa0001abcd12345678 \
-H "Authorization: Bearer scp_YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"name": "Ada Lovelace — Consulting",
"seoDescription": "Mathematician and writer, available for consulting."
}'/cards/{cardId}writePermanently deletes a card (the dashboard shares the same delete — nothing is restorable). Fires card.deleted with {id, name, slug}.
curl -X DELETE https://shepcard.com/api/v1/cards/cmsf3aaaa0001abcd12345678 \ -H "Authorization: Bearer scp_YOUR_TOKEN_HERE"
{ "success": true }/cards/{cardId}/scans?days=30analyticsScan analytics for one card. days (1–365, default 30) controls the range of the daily series and breakdowns; totalScans and recentScans cover all history.
curl "https://shepcard.com/api/v1/cards/cmsf3aaaa0001abcd12345678/scans?days=30" \ -H "Authorization: Bearer scp_YOUR_TOKEN_HERE"
{
"card": { "id": "cmsf3aaaa0001abcd12345678", "name": "Ada Lovelace — Card", "slug": "ada-lovelace-card" },
"totalScans": 42,
"scansThisMonth": 9,
"uniqueVisitors": 3,
"byDay": [
{ "date": "2026-07-06", "scans": 0 },
{ "date": "2026-07-07", "scans": 2 },
{ "date": "2026-07-08", "scans": 1 }
],
"byDevice": [
{ "device": "Mobile", "count": 34 },
{ "device": "Desktop", "count": 8 }
],
"byCountry": [
{ "country": "GH", "count": 30 },
{ "country": "US", "count": 12 }
],
"recentScans": [
{
"id": "cmsf4bbbb0002efgh23456789",
"country": "GH",
"device": "Mobile",
"browser": "Chrome",
"referrer": null,
"createdAt": "2026-08-04T08:12:01.000Z"
}
]
}/webhookswriteRegisters a webhook endpoint. The URL must be https:// — plain HTTP is rejected with 400. The signing secret is included only in this response.
| Field | Type | Required |
|---|---|---|
| url | string (https:// only) | yes |
| events | array: card.created, card.updated, card.deleted, scan.recorded, lead.created | yes |
curl -X POST https://shepcard.com/api/v1/webhooks \
-H "Authorization: Bearer scp_YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/shepcard",
"events": ["card.created", "card.updated", "scan.recorded"]
}'{
"webhook": {
"id": "cmsf5cccc0003ijkl34567890",
"url": "https://example.com/hooks/shepcard",
"events": ["card.created", "card.updated", "scan.recorded"],
"secret": "920a80fc3b31980514133a3cdbfe9a403706dc3ef3cd04f70607d10eef762738",
"active": true,
"createdAt": "2026-08-04T10:05:00.000Z",
"updatedAt": "2026-08-04T10:05:00.000Z"
}
}Pick an endpoint, paste your token, and send a live request from your browser. Your token is used in memory only — it is never stored or logged.
/api/v1/cardsscope: readList your cards. Add ?archived=true to include archived cards.
Tip: create a token in Settings → Developer first. Requests go to /api/v1 on this origin — use the Postman collection or curl for cross-origin workflows.
Webhooks push events to your endpoint the moment they happen. Each delivery is an HTTP POST with a JSON body and three custom headers:
Content-Type: application/json x-shepcard-event: scan.recorded x-shepcard-delivery: 3f9c2c1e-aaaa-bbbb-cccc-3f9c2c1e0000 x-shepcard-signature: <hex HMAC-SHA256 of the raw body>
{
"event": "scan.recorded",
"data": {
"id": "cmsf4bbbb0002efgh23456789",
"cardId": "cmsf3aaaa0001abcd12345678",
"cardSlug": "ada-lovelace-card",
"createdAt": "2026-08-04T08:12:01.000Z",
"ip": "154.160.6.10",
"country": "GH",
"device": "Mobile",
"browser": "Chrome",
"referrer": null
},
"deliveryId": "3f9c2c1e-aaaa-bbbb-cccc-3f9c2c1e0000",
"deliveredAt": "2026-08-04T08:12:01.437Z"
}| Event | Fired when | Payload data |
|---|---|---|
| card.created | A card is created (API or dashboard) | Full card object |
| card.updated | A card is updated (API or dashboard) | Full card object |
| card.deleted | A card is deleted | {id, name, slug} |
| scan.recorded | Someone scans your card | Scan details (id, cardId, device, country, …) |
| lead.created | A lead form is submitted on your card | Lead details (id, cardId, name, email, message) |
Compute HMAC-SHA256 of the raw request body with your webhook's secret and compare it to x-shepcard-signature. Never trust an unverified delivery.
const crypto = require('crypto')
// rawBody: the exact body string — do not re-stringify the parsed JSON
function verifySignature(rawBody, signature, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
if (expected.length !== signature.length) return false
const a = Buffer.from(expected, 'hex')
const b = Buffer.from(signature, 'hex')
return crypto.timingSafeEqual(a, b)
}deliveryId — retries can repeat an eventEach token is limited to 1,000 requests per hour (rolling window). Exceeding the limit returns 429 with a Retry-After header telling you how many seconds to wait.
curl -i https://shepcard.com/api/v1/cards \
-H "Authorization: Bearer scp_YOUR_TOKEN_HERE"
HTTP/1.1 429 Too Many Requests
retry-after: 3590
{ "error": "Rate limit exceeded" }Each token has its own window — create additional tokens per integration if you need more sustained volume.
Errors always return JSON with an error message.
{ "error": "This URL is already taken" }| Status | Meaning |
|---|---|
| 400 | Invalid request — missing or malformed field |
| 401 | Missing or invalid Authorization header / token |
| 403 | Plan does not include the API, token lacks the scope, or card limit reached |
| 404 | Card or webhook not found (non-owned cards return 404 too) |
| 409 | Slug collision — a card with that URL already exists |
| 429 | Rate limit exceeded — see Retry-After |
| 500 | Internal server error — retry; contact support if it persists |
The fastest way to explore the API interactively outside the browser — a ready-made collection covers every endpoint.
Download the collection — shepcard-api.postman_collection.json
Import it in Postman — File → Import (or drag the file in). You get a ShepCard API collection with requests organized under Cards / Analytics / Webhooks.
Set one variable — baseUrl already points at https://shepcard.com/api/v1 and every request carries Authorization: Bearer {{token}}, so the only thing to fill in is token = your scp_… token. Point baseUrl at your dev server instead if you are testing locally.
cardId is saved to a variable automatically.