API Documentation

Inkode integration reference for workspace APIs, Canva, SCIM, and webhooks

Inkode exposes multiple integration surfaces instead of one auth model for every/apiroute. This guide documents the supported surfaces we expose today and clarifies which auth method belongs to each one.

Integrations · Security · Pricing · Compare

Quick start

  1. Configure Canva OAuth for the Inkode app in the Canva Developer Portal.
  2. Use the Canva-managed bearer token against the Canva Library API under /api/canva/*.
  3. Create an optional ink_ API key in Settings > API Keys for direct server-to-server access.
  4. Use SCIM bearer tokens for /api/scim/v2/{orgSlug} provisioning.
curl "https://inkode.io/api/canva/me" \
  -H "Authorization: Bearer oauth_access_token"

Authentication

Match the auth method to the route family

Inkode exposes different auth models for different route families. Workspace endpoints use Clerk sessions, Canva endpoints accept OAuth bearer tokens for the Canva app and optional ink_ API keys for direct integrations, and SCIM provisioning uses separate bearer tokens generated in Security Center.

Clerk session

Cookie-based browser auth

Use same-origin requests from a signed-in Inkode workspace for routes such as /api/qr, /api/links, /api/barcode, /api/org/*, and /api/webhooks/endpoints.

Canva OAuth or API key

Authorization: Bearer <oauth-token>, Bearer ink_..., or X-API-Key

The Canva Library API under /api/canva/* accepts OAuth bearer tokens for the Canva app and workspace-scoped ink_ API keys for direct server-to-server access.

SCIM bearer

Authorization: Bearer scim_...

SCIM tokens authenticate the enterprise provisioning endpoints under /api/scim/v2/{orgSlug}.

Workspace API

Same-origin CRUD and analytics endpoints

These routes power the Inkode dashboard. They are suitable for same-origin browser requests or trusted internal automations that carry a valid workspace session cookie. They are not API-key routes.

GET/api/qr?orgId={orgId}

List QR codes with pagination, search, folder filters, tag filters, and summary totals.

POST/api/qr

Create a QR code. Supports dynamic URL QR creation and Idempotency-Key retries.

GET/api/qr/{id}

Fetch one QR code with linked smart-link metadata when present.

PATCH/api/qr/{id}

Update content, styling, lifecycle settings, short code, or linked smart link.

DELETE/api/qr/{id}

Delete a QR code from the authenticated workspace.

GET/api/qr/{id}/stats?days=30

Read QR analytics, A/B metrics, device and country breakdowns, and routing insights.

GET/api/links?orgId={orgId}

List short links with search, folder/tags, `filter` (all, active, inactive, ab, expiring, health_failing, health_monitored), sort, and pagination.

POST/api/links

Create a short link, optionally attach a QR code, and support Idempotency-Key retries.

GET/api/links/{id}

Fetch one short link with tags, linked QR code, routing profile, and link-page associations.

PATCH/api/links/{id}

Update slug, domain, routing rules, UTM fields, password protection, lifecycle settings, or linked assets.

DELETE/api/links/{id}

Delete a short link from the authenticated workspace.

GET/api/links/{id}/stats?days=30

Read click analytics, referrers, device and country breakdowns, and A/B results.

GET/api/barcode?orgId={orgId}

List barcode assets with search, filters, tags, and pagination.

POST/api/barcode

Create a barcode with normalized type, value validation, folder placement, and optional integration binding.

GET/api/barcode/{id}

Fetch a single barcode with creator details and integration link metadata when available.

PATCH/api/barcode/{id}

Update barcode type, value, style, activity state, folder assignment, or integration mapping.

DELETE/api/barcode/{id}

Delete a barcode from the workspace.

Important

The QR and short-link create endpoints supportIdempotency-Keyheaders to avoid duplicate records on retried requests.

Example

await fetch("/api/qr?orgId=org_123", {
  credentials: "include",
  headers: {
    "Content-Type": "application/json"
  }
})

Canva Library API

OAuth- or API-key authenticated reads and PNG renders

The Canva endpoints are CORS-enabled and accept either an OAuth bearer token, an ink_ API key via Authorization, or X-API-Key. They are intended for Canva asset browsing, lightweight integrations, and image export flows.

GET/api/canva/me

Resolve the current OAuth token or API key to workspace, user, and connection metadata.

GET/api/canva/folders

List non-archived folders available in the authorized workspace.

GET/api/canva/qr?search=&folderId=&limit=

List QR codes scoped to the authorized workspace. limit is clamped between 1 and 50.

GET/api/canva/qr/{qrId}/image?size=1024

Render a QR code as PNG. size is clamped between 256 and 2048.

GET/api/canva/barcode?search=&folderId=&limit=

List barcodes scoped to the authorized workspace. limit is clamped between 1 and 50.

GET/api/canva/barcode/{barcodeId}/image?size=800

Render a barcode as PNG. size is clamped between 256 and 2048.

Example

curl "https://inkode.io/api/canva/qr?search=summer&limit=10" \
  -H "Authorization: Bearer oauth_access_token"

Sample response

{
  "items": [
    {
      "id": "qr_abc123",
      "name": "Summer promo",
      "type": "URL",
      "shortCode": "summer24",
      "isDynamic": true,
      "folderName": "Campaigns"
    }
  ],
  "total": 1
}

Public Endpoints

Conversion tracking and protected access helpers

These routes are designed for public pages rather than signed-in dashboards. They do not use API keys. Instead, they accept structured JSON payloads and manage the visitor or protected-access cookies required by the experience.

POST/api/public/access

Validate a password-protected QR or short link and set the access cookie needed for redirect routes.

POST/api/public/conversions

Record a conversion event from a public page and create a visitor cookie when needed.

Example

curl -X POST "https://inkode.io/api/public/conversions" \
  -H "Content-Type: application/json" \
  -d '{
    "orgId": "org_123",
    "sourceType": "qr",
    "sourceId": "qr_abc123",
    "step": "purchase",
    "value": 129.99,
    "currency": "USD"
  }'

SCIM 2.0

Provision workspace users and role groups

Use SCIM bearer tokens generated in Security Center. The base path is /api/scim/v2/{orgSlug}. Group resources map to workspace roles, and user group assignments can reference either role ids or role names.

GET/api/scim/v2/ServiceProviderConfig

Return the SCIM service-provider capabilities supported by Inkode.

GET/api/scim/v2/Schemas

Return the supported SCIM schemas: User and Group.

GET/api/scim/v2/Schemas/{schemaId}

Fetch a specific schema document.

GET/api/scim/v2/{orgSlug}/Users

List provisioned users. Supports filter values for userName or externalId equality.

POST/api/scim/v2/{orgSlug}/Users

Create or upsert a provisioned user. groups map to workspace role ids or role names.

GET/api/scim/v2/{orgSlug}/Users/{userId}

Fetch one provisioned user.

PUT/api/scim/v2/{orgSlug}/Users/{userId}

Replace a provisioned user record.

PATCH/api/scim/v2/{orgSlug}/Users/{userId}

Patch user fields such as active, emails, displayName, or groups.

DELETE/api/scim/v2/{orgSlug}/Users/{userId}

Deprovision a user and return 204 on success.

GET/api/scim/v2/{orgSlug}/Groups

List SCIM groups backed by workspace roles.

POST/api/scim/v2/{orgSlug}/Groups

Create a SCIM group by role displayName.

GET/api/scim/v2/{orgSlug}/Groups/{groupId}

Fetch one SCIM group.

PATCH/api/scim/v2/{orgSlug}/Groups/{groupId}

Patch a group displayName or membership list.

DELETE/api/scim/v2/{orgSlug}/Groups/{groupId}

Delete a SCIM group and return 204 on success.

Supported user filter

filter=userName eq "person@example.com"orfilter=externalId eq "ext_123"

Example

curl -X POST "https://inkode.io/api/scim/v2/acme/Users" \
  -H "Authorization: Bearer scim_your_token" \
  -H "Content-Type: application/scim+json" \
  -d '{
    "userName": "ops@example.com",
    "displayName": "Ops Manager",
    "active": true,
    "groups": [{ "value": "Admin" }]
  }'

Webhooks

Signed QR lifecycle events

Webhook endpoints are configured in the dashboard and every delivery is signed with the endpoint secret using HMAC-SHA256. Inkode retries failed deliveries up to three times with exponential backoff.

Delivery shape

{
  "event": "qr.created",
  "payload": {
    "id": "qr_abc123"
  },
  "timestamp": "2026-04-02T18:34:11.000Z"
}

Verify the signature

Use the raw request body exactly as received (do not re-serialize JSON). Compare HMAC-SHA256 in hex, prefixed with sha256=.

import crypto from "node:crypto"

/** Return true when X-Inkode-Signature matches the raw POST body. */
export function verifyInkodeWebhookSignature(rawBody, secret, header) {
  const value = (header ?? "").trim()
  if (!value.toLowerCase().startsWith("sha256=")) return false
  const received = value.slice(7)
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody, "utf8")
    .digest("hex")
  if (received.length !== expected.length) return false
  return crypto.timingSafeEqual(
    Buffer.from(received, "utf8"),
    Buffer.from(expected, "utf8")
  )
}

Headers

X-Inkode-Event

The event name, such as qr.created or qr.expired.

X-Inkode-Signature

HMAC-SHA256 signature prefixed with sha256=.

X-Inkode-Delivery

The webhook endpoint id used for delivery tracking.

X-Inkode-Attempt

Retry attempt number starting at 1.

Event catalog

qr.created
qr.updated
qr.deleted
qr.scan_limit_reached
qr.expiry_warning
qr.expired
qr.scanned
link.created
link.updated
link.deleted
link.clicked
barcode.created
barcode.updated
barcode.deleted
barcode.scanned
inventory.movement
inventory.stock_low
inventory.scanned
conversion.recorded
page.viewed

Common errors

400 validation failed or a required field is missing.

401 the access token, API key, or session is missing or invalid.

404 the resource, route parameter, or workspace-scoped asset was not found.

409 the request conflicts with an existing resource such as a reused slug or short code.

422 the request is valid but uses an unsupported value such as an unsupported barcode render.

Operations

Scheduled jobs (Vercel Cron)

These routes are public to Clerk but require CRON_SECRET: send Authorization: Bearer <CRON_SECRET>, header x-cron-secret, or query ?secret=. In production, CRON_SECRET must be configured or handlers return 503. Schedules are defined in vercel.json.

GET/api/cron/notifications

Digest and notification fan-out.

GET/api/cron/qr-alerts

QR alerting sweep.

GET/api/cron/link-health-checks

Scheduled link health checks.

GET/api/cron/weekly-workspace-digest

Weekly workspace summary emails.

GET/api/cron/trial-reminders

Trial expiry reminders.

GET/api/cron/invoice-records-backfill

Stripe invoice row backfill.

GET/api/cron/billing-snapshot-sync

Billing snapshot synchronization.

GET/api/cron/dunning-reminders

Payment recovery reminders.

GET/api/cron/marketing-campaigns

Fire scheduled marketing campaigns.

GET/api/cron/marketing-send-queue

Drain queued marketing sends.

GET/api/cron/expire

Deactivate expired short links and QR codes.

GET/api/cron/downgrade-audit

Enforce plan limits after downgrades; limit-warning emails.

Email marketing

Workspace contacts, campaigns, and POS webhooks

These routes require a signed-in Clerk session and the Integrations-capable plan. Sends use Resend (RESEND_API_KEY) with org/send tags for inbound webhooks. Large broadcasts enqueue in PostgreSQL and drain via /api/cron/marketing-send-queue. Configure RESEND_WEBHOOK_SECRET and point Resend to POST /api/webhooks/resend for bounce and complaint suppression. Clover and Square support OAuth callbacks under /api/integrations/callback/marketing-* (public route; no Clerk required on redirect).

GET/api/org/{orgId}/marketing/overview

Counts: marketable contacts, POS connections, campaigns, sends (30d), queued sends.

GET/api/org/{orgId}/marketing/contacts?limit=50&cursor=

List marketing contacts; cursor pagination by id.

POST/api/org/{orgId}/marketing/contacts

Upsert a contact (body: email, displayName?, marketingConsent?, consentSource?, phone?).

PATCH/api/org/{orgId}/marketing/contacts/{contactId}

Update consent, suppression, or profile fields.

GET/api/org/{orgId}/marketing/campaigns

List campaigns.

POST/api/org/{orgId}/marketing/campaigns

Create a campaign (name, subject, htmlBody, segmentFilter?, scheduledAt?, fromOverride?).

POST/api/org/{orgId}/marketing/campaigns/{campaignId}/send

Enqueue or send: body { maxRecipients?, sync?: boolean }. Default async queue; sync:true sends the full batch in-request.

GET/api/org/{orgId}/marketing/pos-connections

List Clover/Square webhook registrations.

POST/api/org/{orgId}/marketing/pos-connections

Register webhook secrets (squareSignatureKey or cloverSharedSecret) for signature verification.

POST/api/org/{orgId}/marketing/connect/clover

Start Clover OAuth; returns redirectUrl (CLOVER_APP_ID / CLOVER_APP_SECRET).

GET/api/org/{orgId}/marketing/square-oauth-app

Workspace Square Developer app (ID, sandbox; secret stored encrypted).

PUT/api/org/{orgId}/marketing/square-oauth-app

Save workspace Application ID + secret (optional env fallback).

POST/api/org/{orgId}/marketing/connect/square

Start Square OAuth; returns redirectUrl (uses workspace DB credentials or env).

GET/api/integrations/callback/marketing-clover

Clover OAuth redirect (public).

GET/api/integrations/callback/marketing-square

Square OAuth redirect (public).

POST/api/org/{orgId}/marketing/pos-connections/{connectionId}/sync

Backfill customers from Clover or Square using stored OAuth tokens.

POST/api/webhooks/marketing/clover/{connectionId}

Inbound Clover webhook (Authorization: Bearer shared secret).

POST/api/webhooks/marketing/square/{connectionId}

Inbound Square webhook (x-square-hmacsha256-signature).

GET/api/public/marketing/unsubscribe?t=

One-click unsubscribe (no auth).

GET/api/cron/marketing-campaigns

Scheduled campaigns (Authorization: Bearer CRON_SECRET).

GET/api/cron/marketing-send-queue

Process queued marketing sends (Authorization: Bearer CRON_SECRET).

POST/api/webhooks/resend

Resend Svix webhooks: bounce, complaint, failed → suppression + optional send row update.