Enterprise plan
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.
This API manages personal cards. ShepCard Business customer cards are managed in the Business workspace and are excluded from personal API tokens. See the Business guide for customer workflows.
The API is an Enterprise feature. Upgrade from Dashboard → Billing.
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.