Skip to help content
SimpURLHelp Center

Connect tools with API keys

Create a scoped API key for scripts and automation tools, then use the versioned SimpURL REST API.

Create a key for each integration

API keys give an integration access to one account or workspace. They do not replace your browser sign-in or the OAuth setup used by SimpURL MCP.

  1. Choose Personal or the team workspace that owns the resources.
  2. Open Settings > API keys, then select Create key.
  3. Name the integration, select only the permissions it needs, and choose 30, 90, or 180 days.
  4. Copy the key into the tool's secure credential store. Confirm that you have saved it, then select Done.

Use the Developer reference safely

Open the self-hosted Scalar Developer reference at /developers. It provides endpoint search, request and response schemas, examples, and copyable code snippets. The OpenAPI download remains available at the endpoint below for clients that import a specification.

The reference sends requests directly to https://go.simpurl.click/api/v1. It does not route requests through an external proxy. Requests are live: a write creates or changes resources in the selected account or workspace, and every request can count toward the account-wide API limits and plan quotas. Start with a read-only key and GET /account or GET /links before trying a write.

  1. Open /developers from the Help navigation or this guide.
  2. Use the search box to find an endpoint, then review its required permission, parameters, request body, response schema, and example before sending anything.
  3. Enter a key only when you are ready to make a real request. The reference is configured with "persistAuth: false", so manually entered authentication stays in memory only and reload clears it.
  4. For repeatable tests, use the shell examples below with SIMPURL_API_KEY supplied by your shell or secret manager.

Make your first request

Use HTTPS and send the key in the Authorization header as Bearer followed by a space and the key. Put the key in a local environment variable named SIMPURL_API_KEY or a secret manager; the examples below never embed a secret.

The base URL is https://go.simpurl.click/api/v1. The response uses JSON. The account endpoint requires links:read. Creating a link requires links:write and counts toward your normal link allowance.

Read account usage
curl --fail-with-body "https://go.simpurl.click/api/v1/account" \
  -H "Authorization: Bearer $SIMPURL_API_KEY"
Create a link
curl --fail-with-body "https://go.simpurl.click/api/v1/links" \
  -H "Authorization: Bearer $SIMPURL_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"destination_url":"https://example.com/launch","title":"Launch","tags":["launch"]}'

Recipes for links, campaigns, and reports

These examples use the public API contract: create a campaign, create a tagged link assigned to that campaign, request its aggregate report, and page through links with the returned cursor. Prerequisites are curl, jq, and SIMPURL_API_KEY set in the environment. Run the snippets in one shell when passing campaign_id from one recipe to the next, or export that variable after the create step.

The create step assigns campaign_id from .campaign.id in the JSON returned by POST /campaigns. The report recipe uses that same lowercase shell variable. Use set -eu and curl --fail-with-body as shown: a failed campaign request must stop before the script tries to create the link.

  • Permissions: POST /campaigns requires campaigns:write; POST /links requires links:write; GET /campaigns/{id}/analytics requires analytics:read; and GET /links with cursor pagination requires links:read.
Create a tagged campaign link
set -eu
campaign=$(curl --fail-with-body "https://go.simpurl.click/api/v1/campaigns" \
  -H "Authorization: Bearer $SIMPURL_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"name":"Launch","tags":["launch"],"utm_parameters":{"utm_source":"newsletter","utm_campaign":"launch"}}')
campaign_id=$(printf "%s" "$campaign" | jq -r '.campaign.id')
link_payload=$(jq -n --arg campaign_id "$campaign_id" '{destination_url:"https://example.com/launch",title:"Launch",tags:["launch"],campaign_id:$campaign_id}')
curl --fail-with-body "https://go.simpurl.click/api/v1/links" \
  -H "Authorization: Bearer $SIMPURL_API_KEY" \
  -H "Content-Type: application/json" \
  --data "$link_payload"
Read a campaign report
set -eu
: "$campaign_id"
test -n "$campaign_id"
curl --fail-with-body \
  "https://go.simpurl.click/api/v1/campaigns/$campaign_id/analytics?days=30" \
  -H "Authorization: Bearer $SIMPURL_API_KEY" \
  | jq '{campaign: .campaign.name, days, total_clicks, unique_visitors, truncated, mapped_links}'
Loop through link cursors
set -eu
API_BASE="https://go.simpurl.click/api/v1"
cursor=""
while :; do
  if [ -n "$cursor" ]; then
    page=$(curl --fail-with-body --get "$API_BASE/links" \
      -H "Authorization: Bearer $SIMPURL_API_KEY" \
      --data-urlencode "page_size=100" --data-urlencode "tags=launch" \
      --data-urlencode "cursor=$cursor")
  else
    page=$(curl --fail-with-body --get "$API_BASE/links" \
      -H "Authorization: Bearer $SIMPURL_API_KEY" \
      --data-urlencode "page_size=100" --data-urlencode "tags=launch")
  fi
  printf "%s\n" "$page" | jq -c '.links[]'
  cursor=$(printf "%s" "$page" | jq -r '.pagination.next_cursor // empty')
  [ -n "$cursor" ] || break
done

Use the Pages and Profile APIs

Pages and Profile are REST API surfaces with separate permissions. Create a new key with pages:read or pages:write and profile:read or profile:write as needed; existing keys keep their original permissions and do not gain these scopes. Pages requests use the selected personal or team workspace. Profile requests always use the personal account, so a team key cannot use the Profile API.

All Page edits are drafts until you explicitly publish. Page PUT is a full replacement: send name, slug, and content together, with the current revision. A stale revision returns 409; GET the Page, merge your intended change into that latest representation, and retry the replacement. Publish with the current revision, and use the same current-revision discipline for archive changes.

The Profile PUT endpoint is an upsert. It requires handle, display_name, is_public, and show_qr on every request. bio is optional: omit bio to clear it. Omit appearance when you do not intend to change it so the saved appearance is preserved. Profile link endpoints manage placements on the Profile, not the underlying SimpURL.

  • Pages read/write: GET /pages, POST /pages, GET /pages/{id}, PUT /pages/{id}, POST /pages/{id}/publish, POST /pages/{id}/unpublish, and PUT /pages/{id}/archive. GET /pages/{id}/analytics?days=30 requires analytics:read, not pages:read.
  • Pages content: content uses title, description, layout (stack, grid, or editorial), theme (dark or light), accent_color as #hexhex, show_branding, show_qr, and blocks. A link block has id, type: link, link_id, and title.
  • Page writes are replacements, not patches: PUT /pages/{id} accepts name, slug, revision, and content. Archive changes use PUT /pages/{id}/archive with {revision, archived}; publishing is explicit and uses the current revision.
  • For a Page update, GET first and construct the write body from write fields only. Reuse existing cover_image_key and logo_image_key when present, but strip response-only cover_image_url, logo_image_url, and each block's short_url, destination_url, and available fields. A new Page requires a nonempty content.description.
  • Profile read/write: GET /profile and PUT /profile. Profile link placement uses POST /profile/links and PUT /profile/links/{id} with the same placement body, DELETE /profile/links/{id} removes only that placement, and PUT /profile/links/order with ordered_ids containing every placement id.
  • There are no image-upload API operations for Pages or Profiles. Continue using the SimpURL UI for uploads; the REST API covers the documented content and placement fields.
Create and publish a Page draft
set -eu
: "${SIMPURL_API_KEY:?Set SIMPURL_API_KEY first}"
: "${SIMPURL_LINK_ID:?Set SIMPURL_LINK_ID to an existing owned link}"
API_BASE="https://go.simpurl.click/api/v1"
page_payload=$(jq -n --arg link_id "$SIMPURL_LINK_ID" '{name:"Launch resources",slug:"launch-resources",content:{title:"Launch resources",description:"Useful links for the launch",layout:"stack",theme:"light",accent_color:"#0f766e",show_branding:true,show_qr:true,blocks:[{id:"announcement",type:"link",link_id:$link_id,title:"Read the announcement"}]}}')
created=$(curl --fail-with-body "$API_BASE/pages" \
  -H "Authorization: Bearer $SIMPURL_API_KEY" -H "Content-Type: application/json" \
  --data "$page_payload")
PAGE_ID=$(printf "%s" "$created" | jq -r '.page.id // .id // empty')
CURRENT_REVISION=$(printf "%s" "$created" | jq -r '.page.revision // .revision // empty')
test -n "$PAGE_ID"
test -n "$CURRENT_REVISION"
curl --fail-with-body -X POST "$API_BASE/pages/$PAGE_ID/publish" \
  -H "Authorization: Bearer $SIMPURL_API_KEY" -H "Content-Type: application/json" \
  --data "$(jq -n --argjson revision "$CURRENT_REVISION" '{revision:$revision}')"
Update a Page after a 409
set -eu
: "${SIMPURL_API_KEY:?Set SIMPURL_API_KEY first}"
: "${PAGE_ID:?Set PAGE_ID first}"
API_BASE="https://go.simpurl.click/api/v1"
latest=$(curl --fail-with-body "$API_BASE/pages/$PAGE_ID" -H "Authorization: Bearer $SIMPURL_API_KEY")
page=$(printf "%s" "$latest" | jq '.page // .')
revision=$(printf "%s" "$page" | jq -r '.revision // empty')
test -n "$revision"
# Keep all writable content fields, including existing image keys; remove only derived response fields.
replacement=$(printf "%s" "$page" | jq --argjson revision "$revision" '{name,slug,revision:$revision,content} | .content |= (del(.cover_image_url,.logo_image_url) | .blocks |= map(del(.short_url,.destination_url,.available))) | .name = "Updated launch resources"')
curl --fail-with-body -X PUT "$API_BASE/pages/$PAGE_ID" \
  -H "Authorization: Bearer $SIMPURL_API_KEY" -H "Content-Type: application/json" \
  --data "$replacement"
Create or update a personal Profile
set -eu
: "${SIMPURL_API_KEY:?Set SIMPURL_API_KEY first}"
: "${SIMPURL_HANDLE:?Set SIMPURL_HANDLE to the personal handle}"
API_BASE="https://go.simpurl.click/api/v1"
profile_payload=$(jq -n --arg handle "$SIMPURL_HANDLE" '{handle:$handle,display_name:"Jordan Smith",is_public:false,show_qr:true,bio:"Links for my latest work."}')
curl --fail-with-body -X PUT "$API_BASE/profile" \
  -H "Authorization: Bearer $SIMPURL_API_KEY" -H "Content-Type: application/json" \
  --data "$profile_payload"
# To clear bio later, omit bio from the same required-field PUT; omit appearance to preserve the saved appearance.
Place and order a Profile link
set -eu
: "${SIMPURL_API_KEY:?Set SIMPURL_API_KEY first}"
: "${SIMPURL_LINK_ID:?Set SIMPURL_LINK_ID to an existing owned link}"
: "${PROFILE_PLACEMENT_ID:?Set PROFILE_PLACEMENT_ID after creating or reading a placement}"
API_BASE="https://go.simpurl.click/api/v1"
curl --fail-with-body -X POST "$API_BASE/profile/links" \
  -H "Authorization: Bearer $SIMPURL_API_KEY" -H "Content-Type: application/json" \
  --data "$(jq -n --arg link_id "$SIMPURL_LINK_ID" '{link_id:$link_id,title:"My launch link"}')"
curl --fail-with-body -X PUT "$API_BASE/profile/links/order" \
  -H "Authorization: Bearer $SIMPURL_API_KEY" -H "Content-Type: application/json" \
  --data "$(jq -n --arg placement_id "$PROFILE_PLACEMENT_ID" '{ordered_ids:[$placement_id]}')"

Configure an automation tool

Use the HTTP request action in tools such as n8n, Make, or Zapier. This is a generic HTTP integration, not a claim that a native SimpURL connector is installed.

  1. Save the key using the tool's credential or secret field.
  2. Select the HTTP method and an endpoint from the reference. Add Authorization: Bearer <your key> as a protected header.
  3. For POST, PUT, and PATCH requests, send JSON with Content-Type: application/json.
  4. Test an account or link-list read first. Add write permissions only when the workflow needs to create or change resources.

Supported operations

The OpenAPI reference describes accepted fields, parameters, permissions, and errors. Unsupported fields are rejected rather than silently ignored. IDs must belong to the key's account or workspace.

  • GET /account and GET /links: usage and cursor-paginated links. Use search, tags, namespace_ids, campaign_ids, sort, page_size, and cursor. Page size is 1-100; it defaults to 25.
  • POST /links: create a link with destination_url and optional title, tags, namespace_id, slug, campaign_id, and expires_at. The result contains link.id, link.short_code, and link.canonical_path. Build the public URL with https://go.simpurl.click/ and canonical_path when present, otherwise short_code.
  • PATCH /links/{id}/tags replaces tags. PUT /links/{id}/campaign assigns campaign_id; send null to remove the assignment.
  • GET /campaigns, POST /campaigns, and PUT /campaigns/{id}: list, create, or replace campaign settings. For PUT, send every setting you want to preserve. Optional fields omitted from a replacement use their defaults. Timestamps include a timezone; ends_at is the exact closing instant, not an inferred end of day.
  • GET /analytics and GET /campaigns/{id}/analytics: aggregate reports for 7, 14, 30, 90, or 180 days. Raw visitor IP addresses, user-agent strings, and event rows are not exposed. Check truncated before treating a result as complete.
  • GET /namespaces and POST /namespaces: list or create namespaces within plan limits.
  • GET /domain-mappings, POST /domain-mappings, and DELETE /domain-mappings/{id}: manage destination-domain mappings.
  • GET /smart-paste-sites, POST /smart-paste-sites, and DELETE /smart-paste-sites/{id}: manage personal Smart Paste site patterns. These endpoints are not available to team keys.
  • Pages and Profile: see the dedicated Pages/Profile recipe section for workspace and personal-account boundaries, replacement payloads, revisions, placements, and explicit publication.

Handle limits and errors safely

Each account or workspace can have up to 10 active keys. The API allows 100 requests per UTC clock hour, shared across all external keys for that account or Teams workspace, not per key. The allowance resets at the next UTC hour boundary; creating or replacing a key does not reset it. Additional burst protections allow at most 30 writes and 15 analytics requests per minute. Resource quotas and plan restrictions still apply. Dashboard, extension and MCP traffic are unaffected.

X-RateLimit-Limit and X-RateLimit-Remaining show the hourly allowance; X-RateLimit-Reset is the next reset as a Unix timestamp in seconds. Authorized requests count even when validation, a resource error or burst protection rejects them. Invalid credentials, missing scopes, unknown routes, OPTIONS and the public schema do not consume the allowance.

On a rate_limited 429, wait for the actual Retry-After interval before retrying; the delay can be up to 3600 seconds, not a fixed minute. A 429 from a Profile handle change can instead mean the separate policy limit of two changes in a rolling 45-day window; do not rate-retry it. Check the response code and the Profile handle policy before trying again. Avoid blindly repeating writes after a timeout: the original request may already have succeeded. This API does not currently implement Idempotency-Key. An existing destination normally returns HTTP 200 with duplicate: true and a links array instead of creating a new link. Inspect those links before choosing one; use allow_duplicate only when a distinct link is intentional.

  • 400: check the JSON fields, URL parameters, IDs, and timestamp format.
  • 401: key missing, invalid, expired, or revoked.
  • 403: missing permission, inactive plan, or unavailable workspace.
  • 404: the route or resource is not available to this key.
  • 409: custom URL or resource conflict; inspect the error before retrying.
  • 429: account-wide rate limit, or Profile handle policy; check the response code before retrying.
  • 503: temporary service problem; retry reads with backoff and check whether a write completed before repeating it.

Rotate or revoke a key

Keys are immutable: permissions and expiry cannot be extended on an existing secret. Create a replacement with the desired settings, update the integration, verify it, then revoke the old key. The Settings list shows a safe prefix, expiration, status, and last-used date. Activity timestamps can lag by up to five minutes.

Select Revoke and confirm to block subsequent requests using that key. An in-flight request already authorized may finish. Revoked keys cannot be re-enabled. You can revoke keys after a downgrade, even though creating and using keys requires an eligible plan.

A team key is bound to that workspace and its creating owner. It stops working if the workspace loses its active Teams plan or the creating user is no longer an owner. Do not send a workspace header to switch its account; create a separate key instead.