Authentication
Secure your API requests with Bearer tokens
All Tapped API requests require a Bearer token for authentication. Tokens use the prefix ch_live_ and can be scoped to specific permissions.
Generating an API Key
API keys are generated in the Tapped dashboard or programmatically via the API.
Request Body:
| Parameter | Type | Description |
|---|---|---|
name |
string | Required. A descriptive name for the key (e.g., "Shopify Integration") |
scopes |
array | Required. Array of scopes: read, write, admin |
curl -X POST https://api.tappedtags.com/v1/keys \
-H "Authorization: Bearer ch_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Zapier Integration",
"scopes": ["read", "write"]
}'
const response = await fetch('https://api.tappedtags.com/v1/keys', {
method: 'POST',
headers: {
'Authorization': 'Bearer ch_live_YOUR_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Zapier Integration',
scopes: ['read', 'write']
})
});
const data = await response.json();
console.log(data.data.key); // Save this! Shown only once
{
"data": {
"id": "key_abc123",
"name": "Zapier Integration",
"key": "ch_live_xxxxxxxxxxxxxxxxxxxxx",
"scopes": ["read", "write"],
"created_at": "2026-04-11T10:30:00Z"
}
}
Scopes
| Scope | Permissions |
|---|---|
read |
View tags, actions, sequences, schedules, routes, groups, scans, and analytics |
write |
Create, update, and delete tags, actions, sequences, schedules, routes, and groups. Includes read permissions. |
admin |
Manage API keys. Includes read and write permissions. Only available to account admins. |
Using the Token
Include your API key in the Authorization header of every request:
Authorization: Bearer ch_live_xxxxxxxxxxxxxxxxxxxxx
Token Security
- Never share your token in public code, GitHub, or client-side applications
- Rotate keys periodically for security
- Use minimal scopes — only grant permissions you need
- Revoke immediately if a key is compromised
- Store securely in environment variables or a secrets manager
Your First Request
Get up and running in 5 minutes
Step 1: Generate an API Key
- Log in to app.tappedtags.com
- Navigate to Profile → API Keys
- Click Create API Key
- Name it (e.g., "Local Development")
- Select scopes:
readandwrite - Save the key in a
.envfile or secure location
Step 2: Make Your First Request
Let's fetch your list of tags:
curl https://api.tappedtags.com/v1/porters \ -H "Authorization: Bearer ch_live_YOUR_KEY"
const apiKey = process.env.TAPPED_API_KEY;
const response = await fetch('https://api.tappedtags.com/v1/porters', {
headers: {
'Authorization': `Bearer ${apiKey}`
}
});
const { data } = await response.json();
console.log(data); // Array of your tags
import requests
import os
api_key = os.getenv('TAPPED_API_KEY')
headers = {'Authorization': f'Bearer {api_key}'}
response = requests.get(
'https://api.tappedtags.com/v1/porters',
headers=headers
)
data = response.json()
print(data['data']) # Array of your tags
Step 3: Understand the Response
{
"data": [
{
"id": "uuid",
"identifier": "ABCD1",
"nickname": "Conference Badge",
"group_id": "uuid",
"individual_action_id": "uuid",
"use_schedule": false,
"scan_count": 42,
"metadata": { "custom_field": "value" }
}
],
"count": 1
}
Common First Requests
Create a Tag
const tag = await fetch('https://api.tappedtags.com/v1/porters', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
identifier: 'DEMO1',
nickname: 'Demo Tag',
metadata: { department: 'sales' }
})
});
const { data } = await tag.json();
console.log(data.id); // Use this ID for assignments
Assign an Action to a Tag
// Assuming you have an action ID
const actionId = 'action-uuid-here';
const porterId = 'tag-id-from-previous-step';
const assign = await fetch(
`https://api.tappedtags.com/v1/porters/${porterId}/assign`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
type: 'action',
id: actionId
})
}
);
const { data } = await assign.json();
console.log('Assignment successful:', data);
List All Actions
const actions = await fetch('https://api.tappedtags.com/v1/actions', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
const { data } = await actions.json();
console.log(data); // Array of your actions
Best Practices
Patterns for production-ready integrations
Error Handling
Always check the response status and handle errors gracefully:
async function callApi(endpoint, options = {}) {
const response = await fetch(endpoint, {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
...options.headers
},
...options
});
const body = await response.json();
if (!response.ok) {
// Handle errors
const error = body.error || {};
throw new Error(
`API Error ${error.status}: ${error.message}`
);
}
return body.data;
}
// Usage
try {
const tags = await callApi('https://api.tappedtags.com/v1/porters');
console.log('Tags:', tags);
} catch (err) {
console.error('Failed to fetch tags:', err.message);
// Retry logic, alerting, etc.
}
Pagination
For endpoints that return large datasets, use pagination to avoid timeouts:
async function getAllScans(porterId, pageSize = 100) {
const allScans = [];
let offset = 0;
let hasMore = true;
while (hasMore) {
const scans = await callApi(
`https://api.tappedtags.com/v1/porters/${porterId}/scans` +
`?limit=${pageSize}&offset=${offset}`
);
allScans.push(...scans);
// Stop if we got fewer results than requested
hasMore = scans.length === pageSize;
offset += pageSize;
}
return allScans;
}
const allScans = await getAllScans('tag-id');
console.log(`Got ${allScans.length} scans`);
Bulk Operations
When working with multiple tags, use bulk endpoints to reduce API calls:
// INEFFICIENT: 100 API calls
async function slowAssign(tagIds, actionId) {
for (const tagId of tagIds) {
await callApi(`https://api.tappedtags.com/v1/porters/${tagId}/assign`, {
method: 'POST',
body: JSON.stringify({ type: 'action', id: actionId })
});
}
}
// EFFICIENT: 1 API call
async function fastAssign(tagIds, actionId) {
return await callApi(
'https://api.tappedtags.com/v1/porters/bulk/assign',
{
method: 'PATCH',
body: JSON.stringify({
porter_ids: tagIds,
type: 'action',
id: actionId
})
}
);
}
// Tag up to 500 at once
await fastAssign(['tag-1', 'tag-2', ..., 'tag-500'], actionId);
Retry Logic with Exponential Backoff
async function callApiWithRetry(
endpoint,
options = {},
maxRetries = 3
) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(endpoint, {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
...options.headers
},
...options
});
const body = await response.json();
if (!response.ok) {
const error = body.error || {};
// Don't retry on 400, 401, 403, 404
if ([400, 401, 403, 404].includes(error.status)) {
throw new Error(`API Error: ${error.message}`);
}
// Retry on 429 (rate limited), 500, 503
if (![429, 500, 503].includes(error.status)) {
throw new Error(`API Error: ${error.message}`);
}
lastError = new Error(
`API Error ${error.status}: ${error.message}`
);
} else {
return body.data;
}
} catch (err) {
lastError = err;
}
// Wait before retrying: 1s, 2s, 4s
if (attempt < maxRetries - 1) {
const delay = Math.pow(2, attempt) * 1000;
await new Promise(r => setTimeout(r, delay));
}
}
throw lastError;
}
Webhook Validation
When receiving webhook notifications, validate the signature:
const crypto = require('crypto');
function validateWebhookSignature(payload, signature, secret) {
// Create HMAC-SHA256 hash
const hash = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
// Use constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(hash),
Buffer.from(signature)
);
}
// In your webhook handler
app.post('/webhook', (req, res) => {
const signature = req.headers['x-tapped-signature'];
const secret = process.env.WEBHOOK_SECRET;
if (!validateWebhookSignature(req.body, signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook...
res.json({ ok: true });
});
Environment Variables
Never hardcode API keys. Use environment variables:
TAPPED_API_KEY=ch_live_xxxxxxxxxxxxxxxxxxxxx WEBHOOK_SECRET=whk_xxxxxxxxxxxxxxxxxxxxx
require('dotenv').config();
const apiKey = process.env.TAPPED_API_KEY;
if (!apiKey) {
throw new Error('TAPPED_API_KEY environment variable not set');
}
Data Types
Common data structures used throughout the API
Tag (Porter)
| Field | Type | Description |
|---|---|---|
id |
uuid | Unique tag identifier |
identifier |
string | Human-readable tag code (e.g., ABCD1). Used in scan URLs: tapd.ink/T-ABCD1 |
nickname |
string | Custom display name for the tag |
group_id |
uuid | null | Parent group UUID, if any |
individual_action_id |
uuid | null | Direct action assignment (takes precedence) |
use_schedule |
boolean | Whether to use assigned schedule |
use_sequence |
boolean | Whether to use assigned sequence |
use_route |
boolean | Whether to use assigned route |
scan_count |
integer | Total number of scans (read-only) |
metadata |
object | Custom key-value pairs for integration (CRM, webhooks, etc.) |
created_at |
timestamp | Creation timestamp (read-only) |
Action Types
| Type | Primary Data Format | Description |
|---|---|---|
url |
https://example.com |
Redirect scanner to a URL |
vcard |
vCard v4.0 format | Display contact card (name, email, phone, etc.) |
form |
JSON form schema | Collect data via web form |
ab_split |
JSON split config | A/B test two URLs (50/50 split by default) |
wifi |
JSON SSID + password | Share Wi-Fi credentials (NFC only) |
email |
Email address | Open email compose (scanner-dependent) |
phone |
Phone number | Initiate phone call (scanner-dependent) |
Error Codes
Standard HTTP error responses and troubleshooting
| Status | Error Code | Description | Cause |
|---|---|---|---|
| 200 | — | Success | Request completed successfully |
| 400 | bad_request |
Invalid request | Malformed JSON, missing required fields, invalid parameters |
| 401 | unauthorized |
Missing or invalid API key | No Authorization header, invalid token, expired key |
| 403 | forbidden |
Insufficient permissions | API key doesn't have required scope (read/write/admin) |
| 404 | not_found |
Resource not found | Tag, action, or resource doesn't exist |
| 409 | conflict |
Conflict / Invalid state | Trying to delete an action assigned to tags |
| 429 | rate_limited |
Rate limit exceeded | Too many requests in a time window |
Error Response Format
{
"error": {
"status": 400,
"code": "bad_request",
"message": "Missing required field: nickname",
"details": {
"field": "nickname",
"reason": "required"
}
}
}
Common Errors & Solutions
401 Unauthorized
- Missing
Authorizationheader - Invalid API key format (must start with
ch_live_) - Key has been revoked or expired
- Typo in the key
403 Forbidden
- API key missing required scope (e.g., trying to write with
read-only key) - Trying to access another account's data
read and write scopes.
404 Not Found
- Tag ID, action ID, or other resource doesn't exist
- Typo in the UUID or identifier
- Resource was deleted
409 Conflict
- Trying to delete an action assigned to one or more tags
- Invalid state transition
Rate Limits
API request quotas and throttling
Tapped applies rate limits to prevent abuse and ensure fair service:
| Limit Type | Quota | Window |
|---|---|---|
| Requests per minute | 300 | 1 minute (rolling) |
| Requests per day | 100,000 | 24 hours (UTC) |
| Concurrent requests | 50 | — |
| Bulk operation size | 500 items | Per request |
Rate Limit Headers
Every API response includes rate limit information:
X-RateLimit-Limit: 300 X-RateLimit-Remaining: 287 X-RateLimit-Reset: 1681234567
Hitting Rate Limits
When you exceed the rate limit, you'll receive a 429 response:
{
"error": {
"status": 429,
"code": "rate_limited",
"message": "Rate limit exceeded. Try again in 45 seconds."
}
}
Best Practices to Avoid Rate Limits
- Use bulk endpoints instead of making multiple individual requests
- Implement exponential backoff when receiving 429 responses
- Cache responses when data doesn't change frequently
- Batch operations — process 500 items per request
- Monitor rate limit headers and slow down if approaching limits
- Distribute requests evenly across the time window
List Actions
Retrieve all actions in your library
Parameters:
| Parameter | Type | Description |
|---|---|---|
type |
string | Optional. Filter by type: url, vcard, form, ab_split, wifi, email, phone |
limit |
integer | Optional. Max results (default 100) |
{
"data": { ... }
}
Get Action
Retrieve a specific action
Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
uuid | Required. Action UUID |
{
"data": { ... }
}
Create Action
Create a new action in your library
Parameters:
| Parameter | Type | Description |
|---|---|---|
title |
string | Required. Action name |
type |
string | Required. url, vcard, form, ab_split, wifi, email, or phone |
primary_data |
string | Required. Data for the action type |
{"title":"Spring Sale Landing","type":"url","primary_data":"https://example.com/sale"}
{
"data": { ... }
}
Update Action
Modify an existing action
Parameters:
| Parameter | Type | Description |
|---|---|---|
title |
string | Optional. New title |
primary_data |
string | Optional. Updated data |
{"title":"Spring Sale Landing - Updated"}
{
"data": { ... }
}
Delete Action
Delete an action (409 if assigned to tags)
Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
uuid | Required. Action UUID |
{
"data": { ... }
}
List Sequences
Retrieve all action sequences
Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
integer | Optional. Max results |
{
"data": { ... }
}
Get Sequence
Retrieve a specific sequence
Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
uuid | Required. Sequence UUID |
{
"data": { ... }
}
Create Sequence
Create a sequence that rotates through actions on each scan
Parameters:
| Parameter | Type | Description |
|---|---|---|
name |
string | Required. Sequence name |
action_ids |
array | Required. Array of action UUIDs |
mode |
string | Optional. loop (default), linear, one_shot, or random |
{"name":"Daily Promo Rotation","action_ids":["uuid1","uuid2"],"mode":"loop"}
{
"data": { ... }
}
List Schedules
Retrieve all time-based schedules
Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
integer | Optional. Max results |
{
"data": { ... }
}
Get Schedule
Retrieve a specific schedule with all time windows
Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
uuid | Required. Schedule UUID |
{
"data": { ... }
}
Create Schedule
Create a schedule with time-based action routing
Parameters:
| Parameter | Type | Description |
|---|---|---|
name |
string | Required. Schedule name |
timezone |
string | Required. TZ identifier (e.g., America/Los_Angeles) |
windows |
array | Required. Array of time windows |
{"name":"Business Hours","timezone":"America/New_York","windows":[{"start_time":"09:00","end_time":"17:00","day_of_week":"1-5","action_id":"uuid"}]}
{
"data": { ... }
}
List Routes
Retrieve all conditional routes
Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
integer | Optional. Max results |
{
"data": { ... }
}
Get Route
Retrieve a specific conditional route
Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
uuid | Required. Route UUID |
{
"data": { ... }
}
Create Route
Create a conditional routing rule
Parameters:
| Parameter | Type | Description |
|---|---|---|
name |
string | Required. Route name |
fallback_action_id |
uuid | Optional. Default action if no condition matches |
{"name":"Device-Based Routing","fallback_action_id":"uuid"}
{
"data": { ... }
}
List Groups
Retrieve all tag groups
Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
integer | Optional. Max results |
{
"data": { ... }
}
Get Group
Retrieve a specific group with tag count
Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
uuid | Required. Group UUID |
{
"data": { ... }
}
Create Group
Create a new tag group
Parameters:
| Parameter | Type | Description |
|---|---|---|
name |
string | Required. Group name |
active_action_id |
uuid | Optional. Default action for group |
{"name":"Trade Show 2026"}
{
"data": { ... }
}
Assign Action to Group
Set default action for a group
Parameters:
| Parameter | Type | Description |
|---|---|---|
action_id |
uuid | Required. Action UUID |
{"action_id":"uuid"}
{
"data": { ... }
}
Update Group
Modify group name or assignment
Parameters:
| Parameter | Type | Description |
|---|---|---|
name |
string | Optional. New name |
{"name":"Trade Show 2026 - Updated"}
{
"data": { ... }
}
Delete Group
Delete a group (ungroups all tags)
Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
uuid | Required. Group UUID |
{
"data": { ... }
}
List Scans
Retrieve scan events across all tags
Parameters:
| Parameter | Type | Description |
|---|---|---|
porter_id |
uuid | Optional. Filter by tag |
from |
date | Optional. Start date |
to |
date | Optional. End date |
limit |
integer | Optional. Max results |
{
"data": { ... }
}
Analytics Summary
Get account-wide scan statistics
{
"data": { ... }
}
Tag Rankings
Get scan counts ranked by tag
Parameters:
| Parameter | Type | Description |
|---|---|---|
order |
string | Optional. asc or desc (default) |
limit |
integer | Optional. Max results |
{
"data": { ... }
}
List Webhook Configs
Retrieve all webhook profiles (Professional tier)
{
"data": { ... }
}
Create Webhook Config
Create a new webhook profile with field mapping
Parameters:
| Parameter | Type | Description |
|---|---|---|
name |
string | Required. Config name |
url |
string | Required. Webhook URL |
method |
string | Optional. POST (default), PUT, or PATCH |
headers |
object | Optional. Custom HTTP headers |
field_map |
object | Optional. Tapped variable -> custom field mapping |
static_fields |
object | Optional. Static key-value pairs |
include_tags |
boolean | Optional. Include tag data in payload |
{"name":"CRM Integration","url":"https://crm.example.com/webhook","method":"POST"}
{
"data": { ... }
}
Attach Webhook to Tags
Attach a webhook config to tags and/or groups
Parameters:
| Parameter | Type | Description |
|---|---|---|
porter_ids |
array | Optional. Tag UUIDs |
group_ids |
array | Optional. Group UUIDs |
{"porter_ids":["uuid1","uuid2"]}
{
"data": { ... }
}
Test Webhook Config
Send a test webhook payload
{
"data": { ... }
}
List API Keys
List all API keys (admin scope required)
{
"data": { ... }
}
Create API Key
Generate a new API key
Parameters:
| Parameter | Type | Description |
|---|---|---|
name |
string | Required. Key name |
scopes |
array | Required. read, write, and/or admin |
{"name":"Zapier","scopes":["read","write"]}
{
"data": { ... }
}
Revoke API Key
Revoke an API key (admin scope required)
Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
string | Required. Key ID |
{
"data": { ... }
}
Health Check
Check API status (no authentication required)
{
"data": { ... }
}