Dashboard

Authentication

Secure your API requests with Bearer tokens

Authentication 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.

POST https://api.tappedtags.com/v1/keys

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
Example Request
cURL
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"]
  }'
JavaScript
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
Example Response
200 OK
JSON
{
  "data": {
    "id": "key_abc123",
    "name": "Zapier Integration",
    "key": "ch_live_xxxxxxxxxxxxxxxxxxxxx",
    "scopes": ["read", "write"],
    "created_at": "2026-04-11T10:30:00Z"
  }
}
Important: API keys are shown only once. Save the key immediately in a secure location. You cannot retrieve it again.

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 Header
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

  1. Log in to app.tappedtags.com
  2. Navigate to Profile → API Keys
  3. Click Create API Key
  4. Name it (e.g., "Local Development")
  5. Select scopes: read and write
  6. Save the key in a .env file or secure location

Step 2: Make Your First Request

Let's fetch your list of tags:

cURL
curl https://api.tappedtags.com/v1/porters \
  -H "Authorization: Bearer ch_live_YOUR_KEY"
JavaScript (Node.js)
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
Python
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

Response Format
{
  "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

POST /v1/porters
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

POST /v1/porters/:id/assign
// 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

GET /v1/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:

Error Handling Pattern
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:

Pagination Pattern
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:

Bulk Assignment Instead of Individual 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

Retry Strategy
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:

Webhook Signature Validation (Node.js)
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:

.env file
TAPPED_API_KEY=ch_live_xxxxxxxxxxxxxxxxxxxxx
WEBHOOK_SECRET=whk_xxxxxxxxxxxxxxxxxxxxx
Usage in Code
require('dotenv').config();

const apiKey = process.env.TAPPED_API_KEY;

if (!apiKey) {
  throw new Error('TAPPED_API_KEY environment variable not set');
}

List Tags

Retrieve all tags in your account with optional filtering

GET https://api.tappedtags.com/v1/porters

Query Parameters:

Parameter Type Description
group_id uuid Optional. Filter tags by group UUID
limit integer Optional. Max results (default 100, max 500)
offset integer Optional. Pagination offset (default 0)
Example Request
cURL
curl "https://api.tappedtags.com/v1/porters?limit=50&offset=0" \
  -H "Authorization: Bearer ch_live_YOUR_KEY"
JavaScript
const tags = await fetch(
  'https://api.tappedtags.com/v1/porters?limit=50&offset=0',
  {
    headers: { 'Authorization': 'Bearer ch_live_YOUR_KEY' }
  }
).then(r => r.json());

console.log(tags.data); // Array of tags
console.log(tags.count); // Total count
Example Response
200 OK
JSON
{
  "data": [
    {
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "identifier": "ABCD1",
      "nickname": "Conference Badge",
      "group_id": "uuid",
      "individual_action_id": "action-uuid",
      "use_schedule": false,
      "use_sequence": false,
      "use_route": false,
      "scan_count": 42,
      "last_scanned_at": "2026-04-10T15:30:00Z",
      "metadata": {
        "customer_id": "cust_123",
        "department": "sales"
      },
      "active_action_type": "url",
      "active_action_data": "https://example.com",
      "created_at": "2026-01-15T10:00:00Z"
    }
  ],
  "count": 150
}

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 — Summary

Every action has a type field (lowercase string) and a primary_data JSON object whose shape depends on the type. The summary below is for quick reference; for the full primary_data schema of each type, see Action Type Schemas.

Type What scanner sees Tier
url302 redirect to a destination URL (optional query-param injection)Starter
phone302 redirect to tel: URI — phone dialer opensStarter
sms302 redirect to sms: URI — messaging app opens with pre-filled bodyStarter
email302 redirect to mailto: URI — email composer opens with pre-filled subject/bodyStarter
wifiBranded WiFi share page with Android one-tap join + iPhone step-by-stepStarter
vcardBranded contact card page (name, photo, title, all contact methods, social links)Essential
formBranded lead-capture form with definable fields; submission saved to form_submissionsEssential
menuBranded button grid; each button links to another action or an inline targetStarter
htmlRaw HTML page (no Tapped chrome, full creative control)Starter
messageBranded message page (rich HTML body from Quill editor)Starter
ab_splitResolves to variant A or B by weighted random; the chosen action runsProfessional

Action Type Schemas

Complete primary_data structure for every action type

Every action accepted by Create Action and Update Action requires a type string and a primary_data object. The structure of primary_data differs per type and is documented below. Each section shows the exact field schema, an example JSON, variable-interpolation support, and tier requirements.

URL Action

Type: "url" · Tier: Starter (basic URL); Essential for URL Parameters

primary_data fields:

FieldTypeDescription
urlstringRequired. Destination URL (must include scheme).
url_paramsarrayOptional (Essential+). Array of query-parameter rules appended to the URL on each scan. Each entry: { "name": string, "type": "literal" \| "variable", "value": string }. When type is "variable", value is a scan-context key (see Variable Interpolation); when "literal", value is the raw string.
Example primary_data
JSON
{
  "url": "https://example.com/landing",
  "url_params": [
    { "name": "src",        "type": "literal",  "value": "tapped" },
    { "name": "tag_id",     "type": "variable", "value": "identifier" },
    { "name": "customer",   "type": "variable", "value": "customer_label" }
  ]
}

Worker behavior: 302 redirect. If url_params is set, parameters are appended to the destination URL as query string at scan time.

Phone Action

Type: "phone" · Tier: Starter

primary_data fields:

FieldTypeDescription
numberstringRequired. Phone number, ideally with country code (e.g. +15551234567). Whitespace is stripped at scan time.
Example primary_data
JSON
{ "number": "+15551234567" }

Worker behavior: 302 redirect to tel:<number>. Phone dialer opens with the number pre-filled (user still needs to tap call).

SMS Action

Type: "sms" · Tier: Starter

primary_data fields:

FieldTypeDescription
tostringRequired. Recipient phone number. Whitespace stripped at scan time.
bodystringOptional. Pre-filled message text. Supports {{variable}} interpolation.
Example primary_data
JSON
{
  "to": "+15551234567",
  "body": "Hi! I scanned tag {{identifier}} and want more info."
}

Worker behavior: 302 redirect to sms:<to>?body=<url-encoded body>. Messaging app opens with recipient and body pre-filled.

Email Action

Type: "email" · Tier: Starter

primary_data fields:

FieldTypeDescription
tostringRequired. Recipient email address.
subjectstringOptional. Pre-filled subject line. Supports {{variable}} interpolation.
bodystringOptional. Pre-filled body text. Supports {{variable}} interpolation.
Example primary_data
JSON
{
  "to": "sales@acme.com",
  "subject": "Inquiry from tag {{nickname}}",
  "body": "Hi, I scanned {{nickname}} and would like to learn more."
}

Worker behavior: 302 redirect to mailto:<to>?subject=...&body=.... Email composer opens with fields pre-filled.

WiFi Action

Type: "wifi" · Tier: Starter

primary_data fields:

FieldTypeDescription
ssidstringRequired. Network SSID (name).
passwordstringRequired (unless security: "nopass"). Plain-text password on create. Stored encrypted at rest via AES-GCM with PBKDF2 key derivation; decrypted at scan time. On update, an empty value preserves the existing password.
securitystringOptional. One of "WPA" (default), "WEP", or "nopass".
Example primary_data
JSON
{
  "ssid": "GuestWiFi",
  "password": "hunter2!",
  "security": "WPA"
}

Worker behavior: Renders a branded WiFi share page. Android scanners see a one-tap "Connect" button using the WIFI: URI scheme. iPhone scanners see a polished 3-step copy → settings → paste guide. Desktop visitors see the credentials with a copy button.

vCard (Contact Card) Action

Type: "vcard" · Tier: Essential

primary_data fields: (large, nested structure)

FieldTypeDescription
prefix, first_name, middle_name, last_name, suffix, nicknamestringName fields. first_name and last_name are recommended.
job_title, department, company_name, company_headline, biostringProfessional info displayed beneath the name.
profile_picture_url, company_logo_url, banner_image_urlstringImage URLs. Upload via POST /api/upload-asset first (Tapped's R2-hosted assets), then reference the returned URL.
phonesarrayArray of { "type": string, "number": string, "ext"?: string }. type is free-text label like "work", "mobile", "fax".
emailsarrayArray of { "type": string, "address": string }.
websitesarrayArray of { "type": string, "url": string }.
addressesarrayArray of { "type": string, "street": string, "city": string, "state": string, "zip": string, "country": string }. Typically a single entry.
social_linksobjectMap of platform → handle/URL. Keys supported: linkedin, twitter, instagram, facebook, youtube, tiktok, github, snapchat, pinterest, threads.
birthday, anniversarystringDate strings (ISO date or human-readable).
themeobject{ "primary_color": "#hex", "background_color": "#hex", "font_family": string }. Per-action theming. Overridden by brand themes if applied.
Example primary_data (minimal)
JSON
{
  "first_name": "Jane",
  "last_name": "Smith",
  "job_title": "Sales Director",
  "company_name": "Acme Corp",
  "phones": [
    { "type": "work",   "number": "+15551234567" },
    { "type": "mobile", "number": "+15559998888" }
  ],
  "emails": [
    { "type": "work", "address": "jane@acme.com" }
  ],
  "websites": [
    { "type": "work", "url": "https://acme.com" }
  ],
  "social_links": { "linkedin": "jane-smith" },
  "theme": { "primary_color": "#0071e3", "background_color": "#ffffff", "font_family": "Inter" }
}

Worker behavior: Renders a branded contact-card page. Includes "Save to Contacts" button generating a vCard 4.0 file for the scanner's address book. Respects brand theme cascade.

Form (Lead Capture) Action

Type: "form" · Tier: Essential

primary_data fields:

FieldTypeDescription
titlestringForm title displayed at top of page.
descriptionstringOptional. Subtitle / intro text shown below title.
button_labelstringOptional. Submit button text. Default: "Submit".
fieldsarrayRequired. Form field definitions (see below).
redirect_urlstring \| nullOptional. URL to redirect to after submit. If null/empty, the success_message is shown instead.
success_messagestringOptional. Message shown after successful submit when no redirect_url. Default: "Thanks! We'll be in touch."

Field schema (each entry in fields):

FieldTypeDescription
idstringUnique identifier (e.g. "f_a1b2c3"). Used as the key in form submission storage.
typestringOne of: "text", "email", "phone", "number", "textarea", "select", "radio", "checkbox".
labelstringLabel displayed to scanner (e.g. "Email Address").
requiredbooleanWhether the field must be filled before submit.
optionsarrayArray of strings. Required when type is "select", "radio", or "checkbox". Each string is one option label.
Example primary_data
JSON
{
  "title": "Join Our Waitlist",
  "description": "Be the first to know when we launch.",
  "button_label": "Sign Up",
  "fields": [
    { "id": "f_name",    "type": "text",     "label": "Name",    "required": true  },
    { "id": "f_email",   "type": "email",    "label": "Email",   "required": true  },
    { "id": "f_role",    "type": "select",   "label": "Role",    "required": false, "options": ["Founder", "PM", "Engineer", "Other"] },
    { "id": "f_notes",   "type": "textarea", "label": "Anything else?", "required": false }
  ],
  "redirect_url": null,
  "success_message": "Thanks! We'll be in touch."
}

Worker behavior: GET renders the branded form page. POST handles form submission — saves to form_submissions with the porter ID + collected fields, fires both legacy and Webhook Profile webhooks (see Webhooks), then either redirects to redirect_url or renders the success page.

Menu Action

Type: "menu" · Tier: Starter

primary_data fields:

FieldTypeDescription
titlestringMenu title shown at top of page.
subtitlestringOptional. Subtitle shown below title.
buttonsarrayRequired. Up to 8 buttons. See schema below.

Button schema (each entry in buttons):

FieldTypeDescription
labelstringRequired. Button text.
iconstringOptional. Material Icon name (e.g. "phone", "wifi").
icon_colorstringOptional. CSS color string (e.g. "#0071e3").
icon_filledbooleanOptional. Use filled icon variant.
stylestringOptional. Visual style hint.
action_iduuidRequired (one of action_id OR inline). UUID of a library action this button links to. Cannot reference menu or ab_split actions (prevents loops).
inlineobjectRequired (one of action_id OR inline). Inline target object: { "type": "url" \| "phone" \| "email", "value": string }.
Example primary_data
JSON
{
  "title": "Welcome to the Lake House",
  "subtitle": "What can we help you with?",
  "buttons": [
    { "label": "Connect to WiFi",  "icon": "wifi",        "icon_color": "#0071e3", "action_id": "a1b2c3d4-..." },
    { "label": "Local Restaurants", "icon": "restaurant",  "inline": { "type": "url", "value": "https://maps.example.com" } },
    { "label": "Call Host",         "icon": "phone",       "inline": { "type": "phone", "value": "+15551234567" } },
    { "label": "Leave a Review",    "icon": "star",        "action_id": "e5f6a7b8-..." }
  ]
}

Worker behavior: Renders a branded button grid. Each tap navigates to /IDENTIFIER/b/<slot>, which logs a child scan with parent_scan_id and resolves the button's action_id or inline target.

HTML Action

Type: "html" · Tier: Starter

primary_data fields:

FieldTypeDescription
contentstringRequired. Raw HTML served to the scanner. Supports {{variable}} interpolation. No sanitization — content is trusted (owner-authored).
Example primary_data
JSON
{
  "content": "<!DOCTYPE html><html><head><meta name='viewport' content='width=device-width,initial-scale=1'><title>Welcome</title></head><body style='font-family:system-ui;padding:24px;'><h1>Welcome, {{customer_label}}!</h1><p>You scanned {{identifier}}.</p></body></html>"
}

Worker behavior: Returns the raw content with Content-Type: text/html; charset=UTF-8. No Tapped chrome, no CSS injection, no brand theme applied (HTML is intentionally NOT in BRANDABLE_TYPES). If content is empty, serves <p>No content.</p>.

Gotcha: If your HTML uses literal {{...}} patterns (e.g. inline JavaScript templating), they will get replaced or stripped. Avoid by escaping curly braces or structuring code to avoid the pattern.

Message Action

Type: "message" · Tier: Starter

primary_data fields:

FieldTypeDescription
titlestringRequired. Title displayed prominently. Supports {{variable}} interpolation.
contentstringRequired. Rich HTML body (in the dashboard, edited via Quill rich-text editor; the API accepts any HTML). Supports {{variable}} interpolation.
Example primary_data
JSON
{
  "title": "Welcome, {{customer_label}}!",
  "content": "<p>Thanks for scanning <strong>{{nickname}}</strong>.</p><p>Here's what you need to know:</p><ul><li>Item one</li><li>Item two</li></ul>"
}

Worker behavior: Renders a branded message page with the title and HTML content. Respects brand theme cascade. Suitable for short announcements ("Closed for the holidays"), welcome notes, or rich onboarding content.

A/B Split Action

Type: "ab_split" · Tier: Professional

primary_data fields:

FieldTypeDescription
action_a_iduuidRequired. UUID of an existing action (variant A). Must not be another ab_split action.
action_b_iduuidRequired. UUID of an existing action (variant B). Must not be another ab_split action.
weight_aintegerOptional. Percentage chance (0–100) that variant A is served. Default: 50. Variant B gets 100 - weight_a.
Example primary_data
JSON
{
  "action_a_id": "a1b2c3d4-1234-5678-90ab-cdef12345678",
  "action_b_id": "e5f6a7b8-1234-5678-90ab-cdef87654321",
  "weight_a": 70
}

Worker behavior: At scan time, rolls a random number 0–99. If < weight_a, fetches and runs action_a_id; else action_b_id. The variant chosen is logged on the scan record as ab_variant: "A" or "B" for per-variant analytics. If either variant's action is missing, falls back to an error page.

Prerequisites: both variant actions must exist in the owner's library before the ab_split is created.

Variable Interpolation

Scan-context tokens available in action payloads

Several action types support {{token}} interpolation in their text fields. At scan time, the worker replaces each token with the corresponding value from the scan context. Unknown tokens are replaced with empty strings (not left as {{token}}).

Where interpolation is applied

Action TypeFields with interpolation
urlNone (use url_params with type: "variable" instead — see URL schema)
smsbody
emailsubject, body
htmlcontent
messagetitle, content

Available tokens

TokenValue at scan time
{{identifier}}Tag short code (e.g. A6CJB)
{{nickname}}Tag's friendly name (may be empty)
{{group_name}}Tag's group name (or empty string if ungrouped)
{{customer_label}}Customer Identity Name field
{{customer_id}}Customer Identity External ID field
{{campaign_id}}Tag's campaign UUID (or empty)
{{scan_count}}Total scans on this tag (integer)
{{timestamp}}Current scan timestamp (ISO 8601)
{{country}}2-letter country code from Cloudflare geo (may be empty)
{{city}}City from Cloudflare geo (may be empty)
{{region}}Region / state from Cloudflare geo (may be empty)
{{device_type}}Parsed device type (e.g. Mobile, Tablet, Desktop)
{{os}}Operating system (e.g. iOS, Android, Windows)
{{browser}}Browser name
{{action_type}}Resolved action type
{{action_title}}Resolved action's title
{{owner_email}}Tag owner's email (rarely useful in scanner-facing content)

Note on metadata tokens: Tag metadata is also exposed in the webhook payload as metadata.KEY, but is not available as {{metadata.KEY}} in action text fields — only the canonical tokens above. To include custom field values on scan pages, surface them via the form's pre-fill mechanism or use the HTML action with server-side templating in your destination URL.

Example: SMS body with interpolation

JSON
{
  "type": "sms",
  "primary_data": {
    "to": "+15551234567",
    "body": "Hi! I scanned tag {{identifier}} ({{nickname}}) in {{city}} on {{timestamp}}."
  }
}

At scan time on tag A6CJB ("Reception Desk") scanned from Atlanta:

body=Hi! I scanned tag A6CJB (Reception Desk) in Atlanta on 2026-05-29T18:32:00.000Z.

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

Example Error Response
{
  "error": {
    "status": 400,
    "code": "bad_request",
    "message": "Missing required field: nickname",
    "details": {
      "field": "nickname",
      "reason": "required"
    }
  }
}

Common Errors & Solutions

401 Unauthorized

Causes:
  • Missing Authorization header
  • Invalid API key format (must start with ch_live_)
  • Key has been revoked or expired
  • Typo in the key
Solution: Verify your API key in the dashboard. Generate a new key if needed.

403 Forbidden

Causes:
  • API key missing required scope (e.g., trying to write with read-only key)
  • Trying to access another account's data
Solution: Create a new API key with read and write scopes.

404 Not Found

Causes:
  • Tag ID, action ID, or other resource doesn't exist
  • Typo in the UUID or identifier
  • Resource was deleted
Solution: Double-check the ID. Verify the resource exists by listing all resources.

409 Conflict

Causes:
  • Trying to delete an action assigned to one or more tags
  • Invalid state transition
Solution: Remove all tag assignments first, then delete the action.

Rate Limits

API request quotas and throttling

Standard Rate Limits

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:

Response Headers
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:

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
Enterprise customers: Contact support for custom rate limits tailored to your use case.

Bulk Assign to Tags

Assign an action, sequence, schedule, or route to multiple tags (up to 500)

PATCH https://api.tappedtags.com/v1/porters/bulk/assign

Parameters:

Parameter Type Description
porter_ids array Required. Array of tag UUIDs (max 500)
type string Required. 'action', 'sequence', 'schedule', or 'route'
id uuid Required. UUID of the target item
Example Request
JSON
{"porter_ids":["550e...","550f..."],"type":"action","id":"550e8400-e29b-41d4-a716-446655440010"}
Example Response
200 OK
JSON
{
  "data": { ... }
}

Get/Update Tag Metadata

Retrieve or update custom metadata for a tag

GET https://api.tappedtags.com/v1/porters/:id/metadata

Parameters:

Parameter Type Description
id uuid Required (Path). Tag UUID
Example Response
200 OK
JSON
{
  "data": { ... }
}

List Tag Scans

Get scan history for a specific tag

GET https://api.tappedtags.com/v1/porters/:id/scans

Parameters:

Parameter Type Description
from date Optional. Start date (ISO 8601)
limit integer Optional. Max results (default 100)
offset integer Optional. Pagination offset
Example Response
200 OK
JSON
{
  "data": { ... }
}

Deactivate Tag

Soft delete a tag (deactivates it)

DELETE https://api.tappedtags.com/v1/porters/:id

Parameters:

Parameter Type Description
id uuid Required. Tag UUID
Example Response
200 OK
JSON
{
  "data": { ... }
}

List Actions

Retrieve all actions in your library

GET https://api.tappedtags.com/v1/actions

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)
Example Response
200 OK
JSON
{
  "data": { ... }
}

Get Action

Retrieve a specific action

GET https://api.tappedtags.com/v1/actions/:id

Parameters:

Parameter Type Description
id uuid Required. Action UUID
Example Response
200 OK
JSON
{
  "data": { ... }
}

Create Action

Create a new action in your library

POST https://api.tappedtags.com/v1/actions

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
Example Request
JSON
{"title":"Spring Sale Landing","type":"url","primary_data":"https://example.com/sale"}
Example Response
200 OK
JSON
{
  "data": { ... }
}

Update Action

Modify an existing action

PATCH https://api.tappedtags.com/v1/actions/:id

Parameters:

Parameter Type Description
title string Optional. New title
primary_data string Optional. Updated data
Example Request
JSON
{"title":"Spring Sale Landing - Updated"}
Example Response
200 OK
JSON
{
  "data": { ... }
}

Delete Action

Delete an action (409 if assigned to tags)

DELETE https://api.tappedtags.com/v1/actions/:id

Parameters:

Parameter Type Description
id uuid Required. Action UUID
Example Response
200 OK
JSON
{
  "data": { ... }
}

List Sequences

Retrieve all action sequences

GET https://api.tappedtags.com/v1/sequences

Parameters:

Parameter Type Description
limit integer Optional. Max results
Example Response
200 OK
JSON
{
  "data": { ... }
}

Get Sequence

Retrieve a specific sequence

GET https://api.tappedtags.com/v1/sequences/:id

Parameters:

Parameter Type Description
id uuid Required. Sequence UUID
Example Response
200 OK
JSON
{
  "data": { ... }
}

Create Sequence

Create a sequence that rotates through actions on each scan

POST https://api.tappedtags.com/v1/sequences

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
Example Request
JSON
{"name":"Daily Promo Rotation","action_ids":["uuid1","uuid2"],"mode":"loop"}
Example Response
200 OK
JSON
{
  "data": { ... }
}

List Schedules

Retrieve all time-based schedules

GET https://api.tappedtags.com/v1/schedules

Parameters:

Parameter Type Description
limit integer Optional. Max results
Example Response
200 OK
JSON
{
  "data": { ... }
}

Get Schedule

Retrieve a specific schedule with all time windows

GET https://api.tappedtags.com/v1/schedules/:id

Parameters:

Parameter Type Description
id uuid Required. Schedule UUID
Example Response
200 OK
JSON
{
  "data": { ... }
}

Create Schedule

Create a schedule with time-based action routing

POST https://api.tappedtags.com/v1/schedules

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
Example Request
JSON
{"name":"Business Hours","timezone":"America/New_York","windows":[{"start_time":"09:00","end_time":"17:00","day_of_week":"1-5","action_id":"uuid"}]}
Example Response
200 OK
JSON
{
  "data": { ... }
}

List Routes

Retrieve all conditional routes

GET https://api.tappedtags.com/v1/routes

Parameters:

Parameter Type Description
limit integer Optional. Max results
Example Response
200 OK
JSON
{
  "data": { ... }
}

Get Route

Retrieve a specific conditional route

GET https://api.tappedtags.com/v1/routes/:id

Parameters:

Parameter Type Description
id uuid Required. Route UUID
Example Response
200 OK
JSON
{
  "data": { ... }
}

Create Route

Create a conditional routing rule

POST https://api.tappedtags.com/v1/routes

Parameters:

Parameter Type Description
name string Required. Route name
fallback_action_id uuid Optional. Default action if no condition matches
Example Request
JSON
{"name":"Device-Based Routing","fallback_action_id":"uuid"}
Example Response
200 OK
JSON
{
  "data": { ... }
}

List Groups

Retrieve all tag groups

GET https://api.tappedtags.com/v1/groups

Parameters:

Parameter Type Description
limit integer Optional. Max results
Example Response
200 OK
JSON
{
  "data": { ... }
}

Get Group

Retrieve a specific group with tag count

GET https://api.tappedtags.com/v1/groups/:id

Parameters:

Parameter Type Description
id uuid Required. Group UUID
Example Response
200 OK
JSON
{
  "data": { ... }
}

Create Group

Create a new tag group

POST https://api.tappedtags.com/v1/groups

Parameters:

Parameter Type Description
name string Required. Group name
active_action_id uuid Optional. Default action for group
Example Request
JSON
{"name":"Trade Show 2026"}
Example Response
200 OK
JSON
{
  "data": { ... }
}

Assign Action to Group

Set default action for a group

POST https://api.tappedtags.com/v1/groups/:id/assign

Parameters:

Parameter Type Description
action_id uuid Required. Action UUID
Example Request
JSON
{"action_id":"uuid"}
Example Response
200 OK
JSON
{
  "data": { ... }
}

Update Group

Modify group name or assignment

PATCH https://api.tappedtags.com/v1/groups/:id

Parameters:

Parameter Type Description
name string Optional. New name
Example Request
JSON
{"name":"Trade Show 2026 - Updated"}
Example Response
200 OK
JSON
{
  "data": { ... }
}

Delete Group

Delete a group (ungroups all tags)

DELETE https://api.tappedtags.com/v1/groups/:id

Parameters:

Parameter Type Description
id uuid Required. Group UUID
Example Response
200 OK
JSON
{
  "data": { ... }
}

List Scans

Retrieve scan events across all tags

GET https://api.tappedtags.com/v1/scans

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
Example Response
200 OK
JSON
{
  "data": { ... }
}

Analytics Summary

Get account-wide scan statistics

GET https://api.tappedtags.com/v1/analytics/summary
Example Response
200 OK
JSON
{
  "data": { ... }
}

Tag Rankings

Get scan counts ranked by tag

GET https://api.tappedtags.com/v1/analytics/porters

Parameters:

Parameter Type Description
order string Optional. asc or desc (default)
limit integer Optional. Max results
Example Response
200 OK
JSON
{
  "data": { ... }
}

List Webhook Configs

Retrieve all webhook profiles (Professional tier)

GET https://api.tappedtags.com/v1/webhook-configs
Example Response
200 OK
JSON
{
  "data": { ... }
}

Create Webhook Config

Create a new webhook profile with field mapping

POST https://api.tappedtags.com/v1/webhook-configs

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
Example Request
JSON
{"name":"CRM Integration","url":"https://crm.example.com/webhook","method":"POST"}
Example Response
200 OK
JSON
{
  "data": { ... }
}

Attach Webhook to Tags

Attach a webhook config to tags and/or groups

POST https://api.tappedtags.com/v1/webhook-configs/:id/attach

Parameters:

Parameter Type Description
porter_ids array Optional. Tag UUIDs
group_ids array Optional. Group UUIDs
Example Request
JSON
{"porter_ids":["uuid1","uuid2"]}
Example Response
200 OK
JSON
{
  "data": { ... }
}

Test Webhook Config

Send a test webhook payload

POST https://api.tappedtags.com/v1/webhook-configs/:id/test
Example Response
200 OK
JSON
{
  "data": { ... }
}

List API Keys

List all API keys (admin scope required)

GET https://api.tappedtags.com/v1/keys
Example Response
200 OK
JSON
{
  "data": { ... }
}

Create API Key

Generate a new API key

POST https://api.tappedtags.com/v1/keys

Parameters:

Parameter Type Description
name string Required. Key name
scopes array Required. read, write, and/or admin
Example Request
JSON
{"name":"Zapier","scopes":["read","write"]}
Example Response
200 OK
JSON
{
  "data": { ... }
}

Revoke API Key

Revoke an API key (admin scope required)

DELETE https://api.tappedtags.com/v1/keys/:id

Parameters:

Parameter Type Description
id string Required. Key ID
Example Response
200 OK
JSON
{
  "data": { ... }
}

Health Check

Check API status (no authentication required)

GET https://api.tappedtags.com/v1/health
Example Response
200 OK
JSON
{
  "status": "ok",
  "version": "v2",
  "timestamp": "2026-05-27T18:30:00.000Z"
}

Get Tag

Retrieve a single tag by its UUID, including assignment and group context

GET https://api.tappedtags.com/v1/porters/:id

Returns the full tag record from user_fleet_view, which includes assignment status, the resolved active action, group membership, scan counts, and metadata.

Required scope: read

Parameters:

ParameterTypeDescription
id uuid Required. Tag UUID (path parameter)
Example Response
200 OK
JSON
{
  "data": {
    "porter_id": "a7c91b3e-...",
    "identifier": "A6CJB",
    "nickname": "Front Desk Tag",
    "owner_id": "40b318fb-...",
    "group_id": "d2b5...",
    "group_name": "Hotel Lobby",
    "individual_action_id": "e1f4...",
    "active_action_type": "wifi",
    "active_action_title": "Guest WiFi",
    "use_sequence": false,
    "use_schedule": false,
    "use_route": false,
    "scan_count": 142,
    "scan_limit": null,
    "expires_at": null,
    "metadata": { "room": "204" },
    "created_at": "2026-01-15T10:00:00Z",
    "updated_at": "2026-05-20T14:23:00Z"
  }
}

Create Tag

Register a new tag with a unique short-link identifier

POST https://api.tappedtags.com/v1/porters

Creates a new tag owned by the authenticated account. The identifier becomes the short-link path (e.g., tapd.ink/A6CJB) and must be unique across the entire platform. For minting pre-programmed tags in bulk, see Bulk Create.

Required scope: write

Parameters:

ParameterTypeDescription
identifier string Required. Unique short-link identifier. Letters, digits, and hyphens only, max 20 chars.
nickname string Optional. Internal label for the tag.
group_id uuid Optional. Group to place the tag in.
individual_action_id uuid Optional. Action to assign at creation time.
metadata object Optional. Free-form JSON metadata (room number, asset tag, etc.).
Example Request
JSON
{
  "identifier": "LOBBY01",
  "nickname": "Front Desk Tag",
  "group_id": "d2b5...",
  "metadata": { "location": "Front Desk" }
}
Example Response
200 OK
JSON
{
  "data": {
    "id": "a7c91b3e-...",
    "identifier": "LOBBY01",
    "nickname": "Front Desk Tag",
    "group_id": "d2b5...",
    "owner_id": "40b318fb-...",
    "metadata": { "location": "Front Desk" },
    "created_at": "2026-05-27T18:30:00Z"
  },
  "qr_url": "https://tapd.ink/LOBBY01"
}

Bulk Create Tags

Create up to 500 tags in a single request

POST https://api.tappedtags.com/v1/porters/bulk

Designed for fleet rollouts and maker minting workflows. Submit an array of up to 500 tag definitions and receive their full records plus generated QR URLs in a single response.

Required scope: write

Parameters:

ParameterTypeDescription
porters array Required. Array of tag objects (max 500). Each must include identifier. Other fields (nickname, group_id, individual_action_id, metadata) optional.
Example Request
JSON
{
  "porters": [
    { "identifier": "ROOM101", "nickname": "Room 101" },
    { "identifier": "ROOM102", "nickname": "Room 102" },
    { "identifier": "ROOM103", "nickname": "Room 103" }
  ]
}
Example Response
200 OK
JSON
{
  "data": [
    { "id": "...", "identifier": "ROOM101", "qr_url": "https://tapd.ink/ROOM101" },
    { "id": "...", "identifier": "ROOM102", "qr_url": "https://tapd.ink/ROOM102" },
    { "id": "...", "identifier": "ROOM103", "qr_url": "https://tapd.ink/ROOM103" }
  ],
  "count": 3
}

Errors: Returns 400 if any element lacks identifier, if more than 500 tags are submitted, or if any identifier already exists.

Update Tag

Modify a tag's assignment, group, metadata, scan limits, or expiration

PATCH https://api.tappedtags.com/v1/porters/:id

Partial update — only fields included in the body are changed. Use Assign Action instead when you want a single call that switches between action / sequence / schedule / route modes correctly.

Required scope: write

Updatable Fields:

FieldTypeDescription
nicknamestringOptional. Internal label.
group_iduuidOptional. Move tag to this group, or null to ungroup.
individual_action_iduuidOptional. Tag-level action override. Set to null to clear.
use_sequencebooleanOptional. Enable sequence resolution on this tag.
sequence_iduuidOptional. Sequence to use when use_sequence is true.
use_schedulebooleanOptional. Enable schedule resolution.
schedule_iduuidOptional. Schedule to use when use_schedule is true.
use_routebooleanOptional. Enable conditional route resolution.
route_iduuidOptional. Conditional route to use when use_route is true.
scan_limitintegerOptional. Total scan cap (Essential+). null for unlimited.
expires_attimestamptzOptional. ISO 8601 expiration. null for no expiry.
limit_action_iduuidOptional. Action shown once scan_limit/expires_at is reached.
metadataobjectOptional. Replaces metadata wholesale. For partial/merged updates, use the Metadata endpoint.
Example Request — Rename + Move
JSON
{
  "nickname": "Lobby — Renovated",
  "group_id": "f3a7c2..."
}
Example Request — Add Scan Limit
JSON
{
  "scan_limit": 500,
  "expires_at": "2026-12-31T23:59:59Z",
  "limit_action_id": "e1f4..."
}
Example Response
200 OK
JSON
{
  "data": {
    "id": "a7c91b3e-...",
    "identifier": "LOBBY01",
    "nickname": "Lobby — Renovated",
    "group_id": "f3a7c2...",
    "updated_at": "2026-05-27T18:30:00Z"
  }
}

Errors: 400 if the body contains no updatable fields. 404 if the tag does not exist or isn't owned by this account.

Assign Action, Sequence, Schedule, or Route to a Tag

Single-call assignment that correctly sets the mode flags

POST https://api.tappedtags.com/v1/porters/:id/assign

Convenience endpoint that flips a tag into "use action" / "use sequence" / "use schedule" / "use route" mode in one step. Equivalent to a PATCH /porters/:id with the right boolean flags set, but safer because it ensures exactly one resolution mode is active.

Required scope: write

Parameters:

ParameterTypeDescription
type string Required. One of: action, sequence, schedule, route
id uuid Required. UUID of the action / sequence / schedule / route to assign.
Example Request
JSON
{
  "type": "sequence",
  "id": "7c2f4d..."
}
Example Response
200 OK
JSON
{
  "data": {
    "id": "a7c91b3e-...",
    "sequence_id": "7c2f4d...",
    "use_sequence": true,
    "use_schedule": false,
    "use_route": false,
    "individual_action_id": null,
    "updated_at": "2026-05-27T18:30:00Z"
  }
}

Errors: 400 if type is invalid or id missing. 404 if the tag does not exist or isn't owned by this account.

Bulk Update Tag Metadata

Apply the same metadata patch to up to 500 tags at once

PATCH https://api.tappedtags.com/v1/porters/bulk/metadata

Useful for tagging a fleet of tags with a campaign label, asset tag, or batch number in one operation. Defaults to merge mode — existing metadata keys are preserved unless overwritten. Set merge: false to replace each tag's metadata wholesale.

Required scope: write

Parameters:

ParameterTypeDescription
porter_ids array Required. Array of tag UUIDs (max 500).
metadata object Required. Metadata object to apply.
merge boolean Optional. Default true. If false, replaces each tag's metadata entirely.
Example Request
JSON
{
  "porter_ids": ["a7c9...", "b3d4...", "c5e6..."],
  "metadata": { "campaign": "Holiday2026", "venue": "Booth 42" },
  "merge": true
}
Example Response
200 OK
JSON
{
  "data": {
    "count": 3,
    "total_requested": 3
  }
}

Errors: 400 if porter_ids exceeds 500 or contains non-UUID values. 403 if any porter_id isn't owned by this account.

Bulk Move Tags to a Group

Move up to 500 tags into a group (or ungroup them) in a single call

PATCH https://api.tappedtags.com/v1/porters/bulk/group

Reorganize a fleet of tags into a specific group with one call. Pass group_id: null to ungroup the tags entirely.

Required scope: write

Parameters:

ParameterTypeDescription
porter_ids array Required. Array of tag UUIDs (max 500).
group_id uuid | null Required. Destination group UUID, or null to ungroup.
Example Request — Move to Group
JSON
{
  "porter_ids": ["a7c9...", "b3d4..."],
  "group_id": "f3a7c2..."
}
Example Request — Ungroup
JSON
{
  "porter_ids": ["a7c9...", "b3d4..."],
  "group_id": null
}
Example Response
200 OK
JSON
{
  "data": [ ... ],
  "count": 2
}

Errors: 400 if more than 500 IDs submitted, or invalid UUIDs. 403 for tags not owned by the account. 404 if the destination group_id isn't a group owned by the account.

Delete a Tag Metadata Key

Remove a single key from a tag's metadata object

DELETE https://api.tappedtags.com/v1/porters/:id/metadata/:key

Removes the specified key from the tag's metadata JSON object, preserving all other keys. To clear metadata entirely, use the bulk metadata endpoint with merge: false and an empty object, or PATCH the tag with metadata: {}.

Required scope: write

Parameters:

ParameterTypeDescription
id uuid Required. Tag UUID (path parameter).
key string Required. Metadata key to remove (path parameter).
Example Response
200 OK
JSON
{
  "data": {
    "id": "a7c91b3e-...",
    "metadata": { "(updated, key removed)" },
    "updated_at": "2026-05-27T18:30:00Z"
  }
}

Update Sequence

Modify a sequence's name, steps, mode, or active state

PATCH https://api.tappedtags.com/v1/sequences/:id

Partial update. Tags already advancing through the sequence keep their current_index pointer — be careful when reducing action_ids below the existing index, as scanners past the new end will roll over according to the sequence's mode.

Required scope: write

Updatable Fields:

FieldTypeDescription
namestringOptional. Sequence display name.
modestringOptional. One of: loop, linear, one_shot, random.
action_idsarrayOptional. Ordered array of action UUIDs to serve, one per step.
activebooleanOptional. Pause/resume sequence advancement.
tagsarrayOptional. String tags for organization (not scan-tags).
Example Request
JSON
{
  "name": "5-Day Welcome (Updated)",
  "action_ids": ["a1...", "a2...", "a3...", "a4...", "a5..."],
  "mode": "linear"
}
Example Response
200 OK
JSON
{
  "data": { "id": "7c2f4d-...", "name": "5-Day Welcome (Updated)", "updated_at": "..." }
}

Delete Sequence

Permanently delete a sequence (409 if assigned to tags)

DELETE https://api.tappedtags.com/v1/sequences/:id

Returns 409 Conflict if any tag has this sequence assigned. Reassign or clear those tags before deleting.

Required scope: write

Example Response — Success
200 OK
JSON
{
  "data": { "id": "7c2f4d-...", "deleted": true }
}
Example Response — In Use
409 Conflict
JSON
{
  "error": { "message": "Sequence is currently assigned to one or more porters. Reassign them first." }
}

Update Schedule

Modify a schedule's name, windows, timezone, fallback, or override

PATCH https://api.tappedtags.com/v1/schedules/:id

Updates a schedule via the save_schedule RPC, which performs a full upsert of the schedule + its windows. Fields you don't include keep their current values, except windows: passing an empty or missing windows array replaces the current windows with an empty set.

Required scope: write

Updatable Fields:

FieldTypeDescription
namestringOptional. Schedule display name.
timezonestringOptional. IANA timezone (e.g., America/New_York).
action_iduuidOptional. Default action when no window matches.
fallback_action_iduuidOptional. Secondary fallback action.
override_action_iduuidOptional. Action to use when override_active is true.
override_activebooleanOptional. Toggle override mode on/off.
day_of_weekarrayOptional. Days schedule is active (auto-computed from windows if omitted).
start_datedateOptional. ISO date for schedule activation.
end_datedateOptional. ISO date for schedule expiration.
windowsarrayOptional. Array of { start_time, end_time, label, action_id, day_of_week } objects.
Example Request
JSON
{
  "name": "Lunch/Dinner Schedule",
  "timezone": "America/New_York",
  "action_id": "e1f4...",
  "windows": [
    { "start_time": "11:00", "end_time": "15:00", "label": "Lunch", "action_id": "lunch-act...", "day_of_week": [1,2,3,4,5] },
    { "start_time": "17:00", "end_time": "22:00", "label": "Dinner", "action_id": "dinner-act...", "day_of_week": [1,2,3,4,5,6,0] }
  ]
}
Example Response
200 OK
JSON
{
  "data": { "(updated schedule with windows)" },
  "status": "updated"
}

Delete Schedule

Permanently delete a schedule and all its windows

DELETE https://api.tappedtags.com/v1/schedules/:id

Cascades schedule_windows deletion. Returns 409 Conflict if any tag has this schedule assigned.

Required scope: write

Example Response
200 OK
JSON
{ "data": { "id": "...", "deleted": true } }

Update Route

Modify a conditional route's name or fallback action

PATCH https://api.tappedtags.com/v1/routes/:id

The route's conditions are stored separately and edited via the dashboard. This endpoint covers the route-level fields. Route conditions API is on the roadmap.

Required scope: write · Tier: Professional

Updatable Fields:

FieldTypeDescription
namestringOptional. Route display name.
fallback_action_iduuidOptional. Action served when no condition matches.
Example Request
JSON
{
  "name": "Time-of-Day Router",
  "fallback_action_id": "e1f4..."
}
Example Response
200 OK
JSON
{
  "data": { "id": "...", "name": "Time-of-Day Router", "updated_at": "..." }
}

Delete Route

Permanently delete a conditional route (409 if assigned to tags)

DELETE https://api.tappedtags.com/v1/routes/:id

Returns 409 Conflict if any tag has this route assigned. Reassign or clear those tags before deleting.

Required scope: write · Tier: Professional

Example Response
200 OK
JSON
{ "data": { "id": "...", "deleted": true } }

Webhooks Overview

Two webhook systems: reusable configs (Professional) and legacy subscriptions (all tiers)

Tapped exposes two parallel webhook systems, each suited to a different scale of use:

Webhook Configs (Professional tier — recommended)

Reusable webhook configurations with field mapping, static fields, custom HTTP method/headers, and attach-to-many-tags semantics. Designed for fleet-scale integrations where the same endpoint logic applies to dozens or hundreds of tags. A single config can be attached to any combination of individual tags and groups.

Webhook Subscriptions (legacy — all tiers)

Single-URL webhook subscriptions, one per owner. Receive scan events for everything the account owns. Useful for simple "send every scan to my Slack" integrations. See Legacy Subscriptions.

Validation & Security

Outgoing payloads include a timestamp and (when a secret is set on the subscription/config) an HMAC SHA-256 signature in the X-Tapped-Signature header. See Best Practices → Webhook Validation for the verification code template.

Get Webhook Config

Retrieve a single webhook config with its full attached tag and group lists

GET https://api.tappedtags.com/v1/webhook-configs/:id

Returns the config record plus porter_ids (tags this config is attached to) and group_ids (groups this config is attached to).

Required scope: read · Tier: Professional

Example Response
200 OK
JSON
{
  "data": {
    "id": "abc123...",
    "name": "Slack #tag-scans",
    "url": "https://hooks.slack.com/services/...",
    "method": "POST",
    "headers": { "X-Custom": "value" },
    "field_map": { "text": "identifier" },
    "static_fields": { "channel": "#tag-scans" },
    "include_tags": false,
    "active": true,
    "porter_ids": ["a7c9...", "b3d4..."],
    "group_ids": ["f3a7..."]
  }
}

Update Webhook Config

Modify any field of an existing webhook config

PATCH https://api.tappedtags.com/v1/webhook-configs/:id

Partial update — only fields you include are changed. To pause delivery without losing the config, set active: false.

Required scope: write · Tier: Professional

Updatable Fields:

FieldTypeDescription
namestringOptional. Display name for the config.
urlstringOptional. Target endpoint URL.
methodstringOptional. HTTP method (default POST).
headersobjectOptional. Custom HTTP headers as a key/value object.
field_mapobjectOptional. Mapping from outgoing payload keys to scan event fields.
static_fieldsobjectOptional. Static values to include in every payload.
include_tagsbooleanOptional. Include the tag's full metadata + nickname in payloads.
activebooleanOptional. Toggle delivery on/off.
Example Request
JSON
{
  "name": "Slack #tag-scans (paused)",
  "active": false
}

Detach Webhook from Tags

Unbind a webhook config from specific tags and/or groups

POST https://api.tappedtags.com/v1/webhook-configs/:id/detach

Removes the attachment between the config and the specified tags or groups. The config itself remains and can be re-attached later. To delete the config entirely, use Delete Config.

Required scope: write · Tier: Professional

Parameters:

ParameterTypeDescription
porter_idsarrayOptional. Tag UUIDs to detach.
group_idsarrayOptional. Group UUIDs to detach.
Example Request
JSON
{
  "porter_ids": ["a7c9...", "b3d4..."],
  "group_ids": ["f3a7..."]
}
Example Response
200 OK
JSON
{
  "data": {
    "webhook_config_id": "abc123...",
    "detached_porters": 2,
    "detached_groups": 1
  }
}

Delete Webhook Config

Permanently delete a webhook config and all its attachments

DELETE https://api.tappedtags.com/v1/webhook-configs/:id

Removes the config and cascades all porter_webhooks + group_webhooks junction rows. Tags previously attached to this config simply stop receiving its events; they aren't otherwise modified.

Required scope: write · Tier: Professional

Example Response
200 OK
JSON
{ "data": { "id": "abc123...", "deleted": true } }

Legacy Webhook Subscriptions

Simple one-URL-per-event-type webhooks (all tiers)

The legacy /v1/webhooks endpoints provide a simpler webhook model than Webhook Configs: each subscription has a single URL, an array of event types, an optional HMAC secret, and an active flag. Available to all tiers.

For fleet-scale use with per-tag attachment and field mapping, use Webhook Configs instead.

List Subscriptions

GET https://api.tappedtags.com/v1/webhooks

Required scope: read

Create Subscription

POST https://api.tappedtags.com/v1/webhooks

Required scope: write

Body:

FieldTypeDescription
urlstringRequired. Valid HTTPS URL to deliver events to.
labelstringOptional. Display name.
eventsarrayOptional. Event types to subscribe to. Default: ["scan.created"].
secretstringOptional. HMAC secret for payload signing.
Create Example
JSON
{
  "url": "https://example.com/tapped-webhook",
  "label": "Production scan hook",
  "events": ["scan.created"],
  "secret": "whsec_..."
}

Update Subscription

PATCH https://api.tappedtags.com/v1/webhooks/:id

Updatable Fields: label, url, events, secret, is_active

Delete Subscription

DELETE https://api.tappedtags.com/v1/webhooks/:id

Returns 204 No Content on success.

Test Subscription

POST https://api.tappedtags.com/v1/webhooks/:id/test

Fires a synthetic event with event: "test" and an X-Tapped-Event: test header. Returns delivery status, HTTP response code, and any error.

Test Response
200 OK
JSON
{
  "data": {
    "delivered": true,
    "status": 200,
    "url": "https://example.com/tapped-webhook"
  }
}