logo
APIAutomations

Create Smart Rules with the API, MCP, and automations

The rules field on CodeQR links and QR codes — endpoints, full request and response examples, every validation error, the MCP tools that accept rules, and what Make, Zapier, Pluga and webhooks can do.

Avatar for undefined
CodeQR Team
Content Team

At the end of this page you can create, change, read and remove Smart Rules on links and QR codes from your own code, from an AI agent through the MCP server, and from Make, Zapier or Pluga — with the exact payloads and the exact errors the API returns.

Availability

  • Plan: Business and above (pricing). On other plans every request that contains a non-empty rules array is rejected with 403 forbidden.
  • API base URL: https://api.codeqr.io. Auth: Authorization: Bearer <API key> — create a key under SettingsAPI Keys. Reference: docs.codeqr.io/api-reference.
  • MCP server: https://mcp.codeqr.io/mcp (Streamable HTTP, OAuth 2.0). Docs: docs.codeqr.io/mcp.

Before you start

  • An API key with write access to the workspace, and the domain (go.example.com) or an existing linkId.
  • Read the Smart Rules reference for accepted values per attribute — the API stores any string up to 190 characters, so a value the request never carries (mobile, Brazil, BR-SP) is saved and then never matches.

The rules field

rules is an ordered array (max 20 items) on the link and QR code objects. Evaluation is top to bottom, first match wins; no match → the link's url.

  • attribute — Type: enum · Meaning: device, country, city, region, continent, language, referrer, utm_source, utm_medium, utm_campaign, utm_term, utm_content
  • operator — Type: enum · Meaning: equals, not_equals
  • value — Type: string 1–190 · Meaning: compared whole and case-insensitively with what the request carries
  • url — Type: string (URL) · Meaning: destination for matching traffic — or
  • split — Type: array of 2–4 { "url", "weight" } · Meaning: weights are integers 1–100 and must add up to 100

Constraints the server enforces: attribute, operator and value come together or not at all; a rule has url or split, never both; a rule with no condition (all traffic) must split and must be the last item.

UI label → API value:

  • Device / Country / City / Region (state) / Continent / Language / Referrer — In the API: device / country / city / region / continent / language / referrer
  • UTM Source / UTM Medium / UTM Campaign / UTM Term / UTM Content — In the API: utm_source / utm_medium / utm_campaign / utm_term / utm_content
  • is / is not — In the API: equals / not_equals
  • Destination URL (of a rule)** — In the API: url
  • Split traffic (A/B test) with Variant URL and Variant weight (%) — In the API: split[] with url and weight
  • All traffic — In the API: a rule without attribute, operator, value
  • Device value macOS** — In the API: "Mac OS" (also iOS, Android, Windows, Linux)
  • Smart Rules toggle off** — In the API: "rules": null

Endpoints that accept rules

  • POST /links — Use: create a link with rules
  • PUT /links/{linkId} — Use: replace the rules of a link (PATCH is not supported — 405)
  • POST /links/bulk — Use: create many links; each item accepts rules
  • POST /qrcodes — Use: create a dynamic QR code of type url with rules
  • PUT /qrcodes/{qrcodeId} — Use: replace the rules of a QR code
  • POST /qrcodes/bulk — Use: create many QR codes; each item accepts rules

Read rules back with GET /links/{linkId} or GET /links/info?domain=go.example.com&key=get-the-app; the link object includes rules.

Create a link with rules

curl -X POST https://api.codeqr.io/links \
  -H "Authorization: Bearer codeqr_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/app",
    "domain": "go.example.com",
    "key": "get-the-app",
    "rules": [
      { "attribute": "device", "operator": "equals", "value": "iOS",
        "url": "https://apps.apple.com/us/app/example-app/id1234567890" },
      { "attribute": "device", "operator": "equals", "value": "Android",
        "url": "https://play.google.com/store/apps/details?id=com.example.app" }
    ]
  }'

200 OK:

{
  "id": "cmswk759f0001j41i0vj1vmfq",
  "domain": "go.example.com",
  "key": "get-the-app",
  "url": "https://example.com/app",
  "archived": false,
  "expiresAt": null,
  "expiredUrl": null,
  "password": null,
  "externalId": null,
  "trackConversion": false,
  "proxy": false,
  "title": null,
  "description": null,
  "image": null,
  "video": null,
  "utm_source": null,
  "utm_medium": null,
  "utm_campaign": null,
  "utm_term": null,
  "utm_content": null,
  "rewrite": false,
  "doIndex": false,
  "banned": false,
  "flexible": false,
  "filled": false,
  "ios": null,
  "android": null,
  "geo": null,
  "rules": [
    {
      "url": "https://apps.apple.com/us/app/example-app/id1234567890",
      "value": "iOS",
      "operator": "equals",
      "attribute": "device"
    },
    {
      "url": "https://play.google.com/store/apps/details?id=com.example.app",
      "value": "Android",
      "operator": "equals",
      "attribute": "device"
    }
  ],
  "userId": "cm73yc6m70002mtesulffnrgb",
  "folderId": null,
  "projectId": "cm73y7wm100008j24i1b137wr",
  "preRedirection": false,
  "pageId": null,
  "pageUrl": null,
  "isFormMandatory": false,
  "publicStats": false,
  "clicks": 0,
  "lastClicked": null,
  "leads": 0,
  "sales": 0,
  "saleAmount": 0,
  "createdAt": "2026-08-17T01:32:38.884Z",
  "updatedAt": "2026-08-17T01:32:38.884Z",
  "tagId": null,
  "comments": null,
  "notificationToken": null,
  "useAsTemplate": false,
  "tags": [],
  "shortLink": "https://go.example.com/get-the-app",
  "qrCode": "https://api.codeqr.io/qr?url=https://go.example.com/get-the-app?qr=1"
}

Replace the rules of a link and add an A/B split

PUT replaces the whole array — send every rule you want to keep.

curl -X PUT https://api.codeqr.io/links/cmswk759f0001j41i0vj1vmfq \
  -H "Authorization: Bearer codeqr_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "rules": [
      { "attribute": "device", "operator": "equals", "value": "iOS",
        "url": "https://apps.apple.com/us/app/example-app/id1234567890" },
      { "attribute": "device", "operator": "equals", "value": "Android",
        "url": "https://play.google.com/store/apps/details?id=com.example.app" },
      { "split": [
          { "url": "https://example.com/landing-a", "weight": 50 },
          { "url": "https://example.com/landing-b", "weight": 50 }
      ] }
    ]
  }'

200 OK returns the same link object with the three rules; updatedAt changes and the response also carries "webhookIds": [].

Remove all rules

curl -X PUT https://api.codeqr.io/links/cmswk759f0001j41i0vj1vmfq \
  -H "Authorization: Bearer codeqr_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "rules": null }'

200 OK with "rules": null; all traffic goes to url. Sending "rules": [] also stops routing but stores an empty array ("rules": []), and it does not trigger the plan check.

QR codes

Dynamic QR codes of type url accept the same rules:

curl -X POST https://api.codeqr.io/qrcodes \
  -H "Authorization: Bearer codeqr_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "url",
    "url": "https://example.com/menu",
    "domain": "go.example.com",
    "key": "menu",
    "static": false,
    "rules": [
      { "attribute": "language", "operator": "equals", "value": "pt", "url": "https://example.com/cardapio" },
      { "attribute": "language", "operator": "equals", "value": "es", "url": "https://example.com/menu-es" }
    ]
  }'

200 OK returns the QR code object with "static": false and the rules array. With "static": true the request fails: 403 forbiddenSmart rules can only be used on dynamic QR codes.

Create many links at once

POST /links/bulk takes an array of link bodies; each item accepts its own rules.

curl -X POST https://api.codeqr.io/links/bulk \
  -H "Authorization: Bearer codeqr_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '[
    { "url": "https://example.com/app",  "domain": "go.example.com", "key": "get-the-app",
      "rules": [ { "attribute": "device", "operator": "equals", "value": "iOS",
                   "url": "https://apps.apple.com/us/app/example-app/id1234567890" } ] },
    { "url": "https://example.com/menu", "domain": "go.example.com", "key": "menu",
      "rules": [ { "attribute": "language", "operator": "equals", "value": "pt",
                   "url": "https://example.com/cardapio" } ] }
  ]'

200 OK returns an array with one link object per item, each echoing its rules (same shape as the single-link response above). POST /qrcodes/bulk works the same way for dynamic URL QR codes.

Read results per destination

Filter the analytics endpoint by linkId and group by top_urls:

curl -s "https://api.codeqr.io/analytics?event=composite&groupBy=top_urls&linkId=cmswk759f0001j41i0vj1vmfq&interval=24h" \
  -H "Authorization: Bearer codeqr_xxxxxxxxxxxxxxxxxxxxxxxx"

200 OK:

[
  { "url": "https://example.com/app", "clicks": 3, "leads": 0, "sales": 0, "amount": 0, "saleAmount": 0 },
  { "url": "https://apps.apple.com/us/app/example-app/id1234567890", "clicks": 3, "leads": 0, "sales": 0, "amount": 0, "saleAmount": 0 }
]

Use event=clicks for clicks only. Identify the link with linkId (or externalId); with domain and key you must also send type=link, otherwise the endpoint returns an empty array.

Errors

  • 401error.code: unauthorized · error.message: Unauthorized: Login required. · Cause: missing or invalid API key
  • 403error.code: forbidden · error.message: Smart rules are available on the Business plan and above. Upgrade to Business to use this feature. · Cause: non-empty rules from a Free, Starter or Pro workspace
  • 403error.code: forbidden · error.message: Smart rules can only be used on dynamic QR codes. · Cause: static: true with rules
  • 400error.code: unprocessable_entity · error.message: custom: rules[0]: Split weights must add up to 100 · Cause: weights sum ≠ 100
  • 400error.code: unprocessable_entity · error.message: custom: rules: A rule without a condition matches all traffic, so it must be the last one · Cause: split-only rule not last
  • 400error.code: unprocessable_entity · error.message: custom: rules[0]: A rule condition needs attribute, operator and value together · Cause: partial condition
  • 400error.code: unprocessable_entity · error.message: custom: rules[0]: A rule needs either a destination url or a traffic split, never both · Cause: url and split on one rule
  • 400error.code: unprocessable_entity · error.message: custom: rules[0]: A rule without a condition must split traffic · Cause: all-traffic rule with url
  • 400error.code: unprocessable_entity · error.message: invalid_enum_value: rules[0].attribute: Invalid enum value. Expected 'device' | 'country' | … , received 'browser' · Cause: unknown attribute; same pattern for operator
  • 400error.code: unprocessable_entity · error.message: custom: rules[0].url: Invalid URL · Cause: destination is not a URL
  • 400error.code: unprocessable_entity · error.message: too_big: rules[0].value: String must contain at most 190 character(s) · Cause: value too long
  • 400error.code: unprocessable_entity · error.message: too_big: rules: Array must contain at most 20 element(s) · Cause: more than 20 rules
  • 405error.code: — · error.message: — · Cause: PATCH /links/{id}; use PUT
  • 422error.code: unprocessable_entity · error.message: Malicious URL detected · Cause: a rule or split URL failed the destination safety scan

The error body has the shape { "error": { "code", "message", "doc_url" }, "errors": [ … ] }. Note the 400 status with unprocessable_entity in the body for schema errors.

SDKs

The TypeScript SDK @codeqr/ts (import Codeqr from '@codeqr/ts') sends rules on client.links.create(...), client.links.update(linkId, ...), client.qrcodes.create(...), client.qrcodes.update(qrcodeId, ...) and the bulk methods. In version 0.24.1 its Rule type still lists only the seven original attributes (device, country, utm_source, utm_medium, utm_campaign, referrer, language), marks all four fields as required and has no split; the API accepts the full shape, so pass a split or a city/region/continent/utm_term/utm_content rule with a type assertion until the SDK types catch up:

import Codeqr from '@codeqr/ts';

const client = new Codeqr({ apiKey: process.env['CODEQR_API_KEY'] });

const link = await client.links.create({
  url: 'https://example.com/app',
  domain: 'go.example.com',
  key: 'get-the-app',
  rules: [
    { attribute: 'device', operator: 'equals', value: 'iOS',
      url: 'https://apps.apple.com/us/app/example-app/id1234567890' },
    { attribute: 'device', operator: 'equals', value: 'Android',
      url: 'https://play.google.com/store/apps/details?id=com.example.app' },
  ],
});

console.log(link.rules);

There is no Python SDK; call the REST endpoints directly from Python.

MCP

Tools that accept rules (array | null, max 20, same item shape as the API): create_link, update_link. get_link_info (by linkId, externalId, or domain + key) returns the link object including rules. create_qrcode and update_qrcode do not accept rules — create QR codes with rules through the API or the app.

The tool descriptions already state the constraints an agent needs: evaluation order, url xor split, the all-traffic rule, the accepted value formats per attribute ("device: the operating system — "iOS", "Android", "Windows", "Mac OS" or "Linux". Never "mobile" or "desktop": those match nothing"), the sticky window ("A visitor keeps the same variant across visits, for one hour, or 30 days when trackConversion is on.") and that null removes every rule — "on update_link, that is how a running test is ended."

Example prompt and the resulting call:

Create a link on go.example.com to https://example.com/ that sends Brazil to https://example.com/pt-br/ and splits everyone else 50/50 between https://example.com/a and https://example.com/b.

{
  "url": "https://example.com/",
  "domain": "go.example.com",
  "rules": [
    { "attribute": "country", "operator": "equals", "value": "BR", "url": "https://example.com/pt-br/" },
    { "split": [
        { "url": "https://example.com/a", "weight": 50 },
        { "url": "https://example.com/b", "weight": 50 }
    ] }
  ]
}

Every tool returns the API's response object as JSON, so create_link, update_link and get_link_info all hand the agent the full link object including rules. The server also validates rules locally before calling the API and answers Error: … in a sentence for the shape problems listed above. On plans below Business the API rejects the whole call and the agent reports the 403 message.

Automations and webhooks: Make, Zapier, Pluga

  • Make: the CodeQR modules Create a Link and Create a QR Code have no rules field. Use Make an API Call with method POST (or PUT), URL /links (or /links/{linkId}), header Content-Type: application/json and one of the JSON bodies above; the module authenticates with your CodeQR connection.
  • Zapier: the CodeQR app offers the triggers New Link, New Page and New QR Code, the actions Create a Link, Create a QR Code, Delete Link and Delete QR Code, and the searches Retrieve Link and Retrieve a QR Code. Create a Link has no rules field, so to set rules call the API from a Webhooks by Zapier (Custom Request) or Code step with one of the bodies above.
  • Pluga: webhook-based; call the API from an HTTP step.
  • Webhooks: link.created and link.updated payloads contain the link object with its rules (same shape as the API response). link.clicked and qrcode.scanned events carry the click with url set to the destination the visitor was actually sent to — the rule's URL or the split variant — plus the link object. Use them to mirror rule changes and per-destination clicks into your systems.

Verify it works

curl -s https://api.codeqr.io/links/cmswk759f0001j41i0vj1vmfq \
  -H "Authorization: Bearer codeqr_xxxxxxxxxxxxxxxxxxxxxxxx" | python3 -m json.tool | grep -A 12 '"rules"'

curl -sI -A "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) Safari/604.1" https://go.example.com/get-the-app | grep -i location
# location: https://apps.apple.com/us/app/example-app/id1234567890

Changes take effect on the next request — there is no cache delay after a successful save.

Troubleshooting

403 "Smart rules are available on the Business plan and above"

The workspace plan is Free, Starter or Pro. Any request with a non-empty rules array fails, including updates to links that already have rules. Send "rules": null (or omit the field) or upgrade.

400 with error.code "unprocessable_entity"

Schema validation. The message names the failing rule by index (rules[2]) and the field. Compare with the table above; the most frequent are weights not summing to 100, a partial condition, and a split-only rule that is not last.

The rules were saved but nothing matches

The value is not what the request carries: mobile instead of iOS/Android, Brazil instead of BR, BR-SP instead of SP, a full URL instead of a bare domain for referrer, a UTM that is on the destination URL instead of on the short link. See the reference.

405 on update

Use PUT /links/{linkId} (and PUT /qrcodes/{qrcodeId}); PATCH is not supported.

The link's utm_source in the response is null although my rule uses utm_source

The utm_* fields of the link object describe the destination URL. Rules read the query string of the incoming short-link request; they are unrelated fields.

Related articles