Crimbo.AI

API Documentation

Send WhatsApp templates to your clients, download files from your tables, and get a signed webhook whenever something changes in your business. Authenticate with an API key you mint from your business settings.

Base URL

All API requests go to the gateway host:

https://crimbo-gateway-833321703837.me-west1.run.app

Authentication

Every request must include your API key in the X-API-Key header. Keys are issued from your Business Settings page and are scoped to your business.

X-API-Key: crm_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Need a key? Open Business Settings → API Access. The plaintext value is shown exactly once at creation - copy it then or revoke and create a new one.

Scopes

Each key carries one or more scopes that decide what it may do. Scopes are chosen when the key is created and cannot be changed later - to change what an integration may do, create a new key and revoke the old one.

ScopeDescription
messages:sendSend WhatsApp messages: /send-template and /send-action. Keys created without choosing a scope get this one.
collections:readDownload files stored in your tables. Cannot send anything, so it is the safe key to give a webhook receiver.
client_tags:readList your tags and read which tags each client has. Read-only.
client_tags:writeAdd, replace and remove tags on your clients. Keys with this scope can change how your clients are segmented, so give it only to systems you trust with that.

Calling an endpoint your key is not scoped for returns 403:

{
  "error": "This API key does not have the \"collections:read\" scope. Issue a key with that scope in Settings.",
  "code": "API_KEY_SCOPE_MISSING",
  "required_scope": "collections:read"
}

Keeping your key safe

  • Call the API only from your server. Never put a key in a website, a mobile app, or anything else that runs on your customers' devices - anyone can read it there.
  • Keep keys in environment variables or a secrets manager, never in source code or version control, and keep them out of your logs.
  • Give each integration its own key with only the scopes it needs, so you can revoke one without breaking the others.
  • If a key may have leaked, revoke it in Business Settings straight away and create a new one. Revocation takes effect on the next request.

Rate limits

Each API key may make up to 600 requests per minute, measured over a sliding window. Requests from a single IP address are also capped at 1,200 per minute across all keys.

Going over returns 429. Wait a few seconds and retry, backing off further if the 429 repeats.

HTTP/1.1 429 Too Many Requests

{
  "error": "Too many requests",
  "message": "Please slow down and try again shortly."
}

Send a WhatsApp template

POST/api/public/v1/send-template

Sends an approved Meta WhatsApp template to a phone number. If no client with that phone exists for your business, one is auto-created - pass an optional client.name / client.email to populate it.

Request body

FieldTypeRequiredDescription
template_namestringYesApproved Meta template name (case-sensitive).
languagestringYesTemplate language as registered with Meta (e.g. "he", "en_US").
phone_numberstringYesRecipient phone number. E.164 format ("+972…") is preferred; local Israeli numbers are normalised automatically.
parametersstring[]NoPositional values for {{1}}, {{2}}, … in the template body. Order matters.
header_media{ type, url, filename? }NoRequired when the template has an image, video or document header; omit it otherwise. type is "image", "video" or "document" and must match the template's header. url must be a public https link: WhatsApp downloads the file from it. filename is optional and for documents only.
button_parameters{ index, value, type? }[]NoRequired when a button's link ends in {{1}}; otherwise needed only to set a quick-reply payload. index is the button's 0-based position in the template. For a link, value is only the part that replaces {{1}}: WhatsApp adds it to the end of the link the template declares. type is "url" (the default), or "quick_reply" to set the payload you get back when the customer taps that button.
client.namestringNoDisplay name used when auto-creating a client. Ignored if a client with this phone already exists.
client.emailstringNoEmail used when auto-creating a client. Ignored if a client with this phone already exists.

Example request

curl -X POST https://crimbo-gateway-833321703837.me-west1.run.app/api/public/v1/send-template \
  -H "X-API-Key: $CRIMBO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template_name": "appointment_reminder",
    "language": "he",
    "phone_number": "+972501234567",
    "parameters": ["John", "10:00"],
    "client": { "name": "John Doe" }
  }'

Templates with a video, image or document header

WhatsApp rejects such a template with error 132012 unless the send names the file. Pass header_media with a public https link; the type must match the template's header. A 200 response only means WhatsApp accepted the request: a link it cannot use (unreachable, too large, or a video that is not mp4 with H.264 video and AAC audio) fails later, as error 131053 in the delivery-status webhook.

curl -X POST https://crimbo-gateway-833321703837.me-west1.run.app/api/public/v1/send-template \
  -H "X-API-Key: $CRIMBO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template_name": "product_launch",
    "language": "en_US",
    "phone_number": "+972501234567",
    "header_media": { "type": "video", "url": "https://example.com/launch.mp4" }
  }'

Buttons with a dynamic link

If a button's link in the template ends in {{1}} (for example https://example.com/invoices/{{1}}), pass button_parameters with the button's index and only the part that replaces {{1}}. Send "abc123", not the whole address: WhatsApp appends your value to the link the template declares, and nothing checks that the result works. Line breaks, tabs and repeated spaces in a value become a single space.

curl -X POST https://crimbo-gateway-833321703837.me-west1.run.app/api/public/v1/send-template \
  -H "X-API-Key: $CRIMBO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template_name": "invoice_ready",
    "language": "en_US",
    "phone_number": "+972501234567",
    "parameters": ["John"],
    "button_parameters": [{ "index": 0, "value": "abc123" }]
  }'

Example response (200 OK)

{
  "success": true,
  "message_id": "wamid.HBgLOTcyNTAxMjM0NTY3FQIAERgSMjA4Q0...",
  "client_id": 1234,
  "client_created": false
}

message_id is Meta's WAMID for the dispatched template. client_created tells you whether the call created a new client row.

Send with your own payload shape

POST/api/public/v1/send-action

Use this instead of /send-template when your system already emits its own payloads. You keep sending your own action names and your own field names - nested however you like - and we translate them into the template Meta expects. Same API key, same delivery, same response.

It works off a one-time setup: your partner declares which template each action sends and which of your fields fills each of the template's variables, on their partner page. That configuration covers every business they manage, so onboarding another business needs no change on your side.

Request body

FieldTypeRequiredDescription
actionTypestringYesThe action name your partner configured. Matched case-insensitively.
recipientstringYesRecipient phone number. E.164 format ("+972…") is preferred; local Israeli numbers are normalised automatically. phone_number is accepted as a synonym.
languagestringNoTemplate language. Falls back to the language configured for the action, and is required when the action has none.
…any other fieldstring | number | booleanNoEverything else is yours. Nest it however your system already does - the configuration points at each value by path, e.g. customer.customerName or extraFields.invoiceUrl.

Fields like business.businessName or customer.customerName are values only. They fill message variables; they never decide which business sends or which client receives. Your API key identifies the sender and recipient identifies the customer.

Example request

curl -X POST https://crimbo-gateway-833321703837.me-west1.run.app/api/public/v1/send-action \
  -H "X-API-Key: $CRIMBO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "actionType": "OPTIMUS_UPDATE_EVENT_TIME",
    "recipient": "0501234567",
    "language": "he",
    "customer": {
      "customerName": "יוסי כהן"
    },
    "appointment": {
      "eventDate": "15/06/26",
      "eventStartTime": "10:00"
    },
    "business": {
      "businessName": "מספרת דנה",
      "businessPhone": "031234567"
    }
  }'

Another action, with a link

A different action needs different fields, and nothing about the request changes but the body. A value like an invoice URL can fill a message variable or the dynamic part of a button's link, depending on how the action was configured.

One thing to know if a value feeds a button link: WhatsApp adds it to the end of the link the template already declares, so a template linking to https://example.com/{{1}} wants just the reference. Keep sending whole addresses if that is what you have - your partner tells us which start to remove, and a link pointing somewhere unexpected is rejected rather than sent broken.

{
  "actionType": "OPTIMUS_INCOMING_INVOICE_NO_BUTTON",
  "recipient": "0501234567",
  "language": "he",
  "business": {
    "businessName": "מספרת דנה",
    "businessPhone": "031234567"
  },
  "extraFields": {
    "invoiceUrl": "https://example.com/invoices/abc123"
  }
}

Example response (200 OK)

{
  "success": true,
  "message_id": "wamid.HBgLOTcyNTAxMjM0NTY3FQIAERgSMjA4Q0...",
  "client_id": 1234,
  "client_created": false
}

When a field is missing

A mapped field that is absent or empty fails the request before anything is sent, because WhatsApp rejects a message with an empty variable anyway. Every missing field is listed at once, each naming the path it was expected at, so one round-trip tells you everything to fix. Your partner can set a fallback value per field to make one optional.

{
  "error": "Payload is missing values for: body variable {{1}} (expects \"customer.customerName\")",
  "code": "ACTION_FIELD_MISSING",
  "missing_fields": [
    { "target": "body", "position": 1, "path": "customer.customerName" }
  ]
}

Download a file from a table

GET/api/public/v1/collections/:collectionId/rows/:rowId/files/:columnKey

Fetches the file stored in one cell of a table. This is the downloadUrl that every file cell carries in a collection_row webhook, so you rarely build it yourself. Requires the collections:read scope.

The response is a 302 redirect to a signed storage link that expires after 15 minutes. The downloadUrl itself never expires, so it is safe to store and fetch later. Add ?disposition=inline to have the file shown in the browser rather than downloaded.

# 1. Ask for the file. The key goes only to us; the answer is a redirect.
read -r STATUS SIGNED_URL < <(curl -s -o response.json -w '%{http_code} %{redirect_url}' \
  -H "X-API-Key: $CRIMBO_API_KEY" \
  https://crimbo-gateway-833321703837.me-west1.run.app/api/public/v1/collections/12/rows/4471/files/quote)
if [ "$STATUS" != "302" ]; then
  echo "No download link (HTTP $STATUS):" >&2; cat response.json >&2; exit 1
fi

# 2. Download from the signed link, without the key.
curl -f -o quote.pdf "$SIGNED_URL"

Do not let your HTTP client follow the redirect automatically with the key attached: curl -L, and many libraries, would send your X-API-Key to the storage host too. Read the Location header, then fetch it without the key. The signed link works for anyone who has it until it expires, so do not log it or pass it on.

Client tags

Read and change the tags on your clients — for example to segment a client from your CRM the moment they pay. Tags are matched by name and are the same ones you manage in the dashboard.

Tags are created in the dashboard (Classifications), not through the API. Using a tag name that does not exist is an error, and neither tags nor clients are ever created by these calls.

MethodEndpointScopeDescription
GET/api/public/v1/tagsclient_tags:readList the tags your business has defined.
GET/api/public/v1/clients/tags?phone_number=…client_tags:readGet the tags one client currently has.
POST/api/public/v1/clients/tagsclient_tags:writeAdd tags to a client. Tags the client already has are left as they are, so repeating a call is safe.
PUT/api/public/v1/clients/tagsclient_tags:writeReplace the client's whole tag set with exactly the tags you send. An empty array removes every tag.
DELETE/api/public/v1/clients/tagsclient_tags:writeRemove the listed tags from a client. Tags the client does not have are ignored. Send the JSON body on the DELETE request.

Request body

FieldTypeRequiredDescription
phone_numberstringYesThe client's phone number, normalised the same way as in /send-template. It goes in the body (or, for GET, the phone_number query parameter) and never in the URL path. In a query string, write a leading + as %2B.
tagsstring[]YesTag names, as shown in your dashboard. Matched case-insensitively, ignoring surrounding spaces; duplicates are ignored. Every entry must be a non-empty string; at most 100 names per request, each at most 100 characters. Not used by GET.

Example requests

# Add tags (POST). PUT replaces the whole set; DELETE removes the listed tags.
curl -X POST https://crimbo-gateway-833321703837.me-west1.run.app/api/public/v1/clients/tags \
  -H "X-API-Key: $CRIMBO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "0501234567",
    "tags": ["VIP", "newsletter"]
  }'

# Read one client's tags (GET) and list all your tags
curl -H "X-API-Key: $CRIMBO_API_KEY" \
  "https://crimbo-gateway-833321703837.me-west1.run.app/api/public/v1/clients/tags?phone_number=%2B972501234567"
curl -H "X-API-Key: $CRIMBO_API_KEY" https://crimbo-gateway-833321703837.me-west1.run.app/api/public/v1/tags

Example response (200 OK)

{
  "client_id": 1234,
  "tags": [
    { "id": 7, "name": "VIP", "color": "#FF5733" },
    { "id": 12, "name": "newsletter", "color": "#3366FF" }
  ]
}

POST, PUT, DELETE and GET on /clients/tags all answer with the client's resulting tag set. GET /tags answers with the same tag objects under a tags key, for every tag your business has defined.

{
  "tags": [
    { "id": 7, "name": "VIP", "color": "#FF5733" },
    { "id": 12, "name": "newsletter", "color": "#3366FF" }
  ]
}

All or nothing

A write either applies completely or not at all. If any name in tags does not exist, nothing is changed and the call returns 404 TAG_NOT_FOUND with every unknown name in missing_tags. A phone number with no client returns 404 CLIENT_NOT_FOUND.

{
  "error": "One or more tags do not exist. Create them in the dashboard first.",
  "code": "TAG_NOT_FOUND",
  "missing_tags": ["prospect"]
}

Webhooks

Webhooks send an HTTP POST to your URL when something happens in your business. Each webhook watches one kind of resource and the events you pick for it.

Webhooks are set up in the dashboard: Integrations → Developer → Custom webhooks. That is also where you find each webhook's signing secret, a sample payload for every event, and its last 100 deliveries.

Events

EventDescription
task.created · task.updated · task.deletedA task was created, changed or deleted - by the AI agent or by someone on your team.
calendar_event.created · calendar_event.updated · calendar_event.deletedA meeting was booked, moved or cancelled.
collection_row.created · collection_row.updated · collection_row.deletedA row was added, changed or removed in the one table the webhook watches. Cells arrive under data.values, keyed by your column keys.
template_reply.receivedSomeone tapped a quick-reply button on a WhatsApp template you sent. Carries the reply text, their phone number and the button payload.
client.createdA new client was added: a first message on any channel, a lead form, this API or the dashboard. Carries clientId, fullName, phoneNumber, email, source, tags, platformUserId, isActive and createdAt. CSV imports are not sent. Rarely the same client can arrive twice, so de-duplicate on data.clientId.

Payload

Unless you set a payload template, every delivery is a JSON object with these fields:

FieldTypeAlways presentDescription
eventstringYesThe event type, e.g. "task.updated".
businessIdnumberYesYour business id.
deliveryIdstring (UUID)YesUnique per delivery. Store it to ignore a delivery you have already processed.
timestampstring (ISO 8601)YesWhen the delivery was sent, in UTC.
dataobjectYesThe resource itself: the task, calendar event, table row, quick reply or new client, including the client it belongs to where there is one.
changesobjectNoOn *.updated events only: each changed field with its before and after value.
{
  "event": "task.updated",
  "businessId": 42,
  "deliveryId": "3f1c2a9e-8b7d-4c51-9a0e-2d6f4b8e1c07",
  "timestamp": "2026-10-02T17:30:00.000Z",
  "data": {
    "taskId": 1234,
    "businessId": 42,
    "clientId": 88,
    "client": {
      "id": 88,
      "fullName": "Dana Levi",
      "phoneNumber": "+972501234567",
      "email": "dana@example.com"
    },
    "title": "Call back about the quote",
    "content": "Asked for a price on the premium package.",
    "priority": 2,
    "dueDate": "2026-10-04T09:00:00.000Z"
  },
  "changes": {
    "priority": { "before": 1, "after": 2 }
  }
}

The example is shortened. The dashboard shows the full sample for every event, built from your own tables for collection_row events.

Headers

Every delivery is sent with Content-Type: application/json, any custom headers you added, and these headers:

HeaderDescription
X-Webhook-EventThe event type. Same value as event in the body.
X-Webhook-DeliveryThe delivery id. Same value as deliveryId in the body.
X-Webhook-Signaturesha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with the webhook secret.
User-AgentAlways CrimboWebhooks/1.0.

Verifying the signature

Anyone who knows your URL can POST to it, so check the signature before trusting a delivery. Compute an HMAC-SHA256 of the raw request body using the webhook secret, hex-encode it, prefix it with sha256=, and compare it to X-Webhook-Signature with a constant-time comparison. Use the body exactly as received: parsing and re-serialising the JSON changes the bytes and breaks the match.

A valid signature proves we sent the body, not that it is being sent for the first time. To stop a captured delivery from being replayed, reject any whose timestamp is more than 5 minutes old, and remember each deliveryId you have processed so a repeat is ignored. Both values are inside the signed body, so they cannot be altered.

Node.js (Express)

const crypto = require('crypto');
const express = require('express');

const app = express();
const SECRET = process.env.CRIMBO_WEBHOOK_SECRET;
if (!SECRET) throw new Error('CRIMBO_WEBHOOK_SECRET is not set');

const MAX_AGE_MS = 5 * 60 * 1000;

// express.raw keeps the body as bytes: the signature covers them exactly as sent.
app.post('/webhooks/crimbo', express.raw({ type: 'application/json', limit: '1mb' }), async (req, res) => {
  if (!Buffer.isBuffer(req.body)) return res.status(400).end(); // not JSON: not from us

  const expected = Buffer.from(
    'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex')
  );
  const received = Buffer.from(req.get('X-Webhook-Signature') || '');
  if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
    return res.status(401).end();
  }

  const event = JSON.parse(req.body);
  const ageMs = Date.now() - Date.parse(event.timestamp);
  if (!(Math.abs(ageMs) <= MAX_AGE_MS)) {
    return res.status(400).end(); // missing, too old or in the future: possibly replayed
  }
  // One atomic step, e.g. INSERT into a table with a unique deliveryId column.
  // A separate "check, then save" lets two copies through at the same moment.
  if (!(await recordDelivery(event.deliveryId))) {
    return res.status(200).end(); // duplicate: acknowledge, do nothing
  }

  await enqueue(event); // your job queue; do the real work there
  res.status(200).end(); // answer within 5 seconds
});

Python (Flask)

import hashlib, hmac, json, os
from datetime import datetime, timedelta, timezone
from flask import Flask, request, abort

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 1024 * 1024
SECRET = os.environ["CRIMBO_WEBHOOK_SECRET"].encode()
MAX_AGE = timedelta(minutes=5)

@app.post("/webhooks/crimbo")
def crimbo_webhook():
    body = request.get_data()
    expected = b"sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest().encode()
    received = request.headers.get("X-Webhook-Signature", "").encode()
    if not hmac.compare_digest(received, expected):
        abort(401)

    try:
        event = json.loads(body)
        sent_at = datetime.fromisoformat(event["timestamp"].replace("Z", "+00:00"))
        age = abs(datetime.now(timezone.utc) - sent_at)
    except (ValueError, KeyError, TypeError, AttributeError):
        abort(400)  # malformed, or a timestamp without a timezone
    if age > MAX_AGE:
        abort(400)  # too old or in the future: possibly replayed
    # One atomic step, e.g. INSERT into a table with a unique deliveryId column.
    if not record_delivery(event["deliveryId"]):
        return "", 200  # duplicate: acknowledge, do nothing

    enqueue(event)  # your job queue; do the real work there
    return "", 200  # answer within 5 seconds

Securing your endpoint

  • Use an https:// URL. We refuse plain http, and addresses on private networks, for webhooks.
  • Treat the signing secret like a password: store it the way you store API keys. If it may have leaked, delete the webhook and create a new one to get a new secret.
  • Treat the contents of data as untrusted input even after the signature checks out. Names, task text and quick-reply text are typed by your customers and your team, so escape them before showing them in HTML and use parameterised queries when saving them.
  • Do not return error details or stack traces in your response. We store only the status code, so a bare 401 or 400 is enough.

Delivery and retries

Each event is sent once. Any 2xx response within 5 seconds counts as delivered. A timeout, a non-2xx response or an unreachable URL is recorded as a failure in the delivery history, and the event is not retried - so answer quickly and do the real work in the background. Redirects are not followed either: a 3xx response counts as a failure, so register the final URL.

Deliveries are not guaranteed to arrive in order. Use timestamp to tell which change is newer, and deliveryId to skip anything you have already handled.

Saving a webhook and "Send test event" both deliver synthetic data marked with data._test: true, through the same headers, template and signature as a real event. Ignore those on your side if you do not want test rows.

Payload templates

To post straight into another service such as Monday or Notion, give the webhook a payload template: the JSON you want sent, with placeholders for our values. {{path}} inserts a value as an escaped string, for use inside quotes. {{{path}}} inserts it as-is, for numbers, booleans and whole objects. Paths start from event, timestamp, businessId, deliveryId, data and changes.

{
  "name": "{{data.title}}",
  "priority": {{{data.priority}}},
  "client_phone": "{{data.client.phoneNumber}}",
  "source_event": "{{event}}",
  "deliveryId": "{{deliveryId}}",
  "timestamp": "{{timestamp}}"
}

The signature covers the rendered template, so verification works the same way with or without one. Keep {{deliveryId}} and {{timestamp}} in your template, as in the example: the X-Webhook-Delivery header is not signed, so without them in the body you cannot check for replays.

Error codes

Errors return a JSON body with stable error and code fields plus a human-readable error message. The HTTP status reflects the category.

{
  "error": "Invalid or revoked API key.",
  "code": "API_KEY_INVALID"
}
StatusCodeDescription
401API_KEY_MISSINGNo X-API-Key header was sent.
401API_KEY_INVALIDThe key is unknown, revoked, or malformed.
403API_KEY_SCOPE_MISSINGThe key is valid but lacks the scope this endpoint needs. The response names it in `required_scope`.
400TEMPLATE_NAME_REQUIREDtemplate_name is missing or empty.
400LANGUAGE_REQUIREDlanguage is missing or empty.
400PHONE_NUMBER_REQUIREDphone_number is missing or empty.
400PHONE_NUMBER_INVALIDThe phone number could not be normalised to a valid form.
400PARAMETERS_INVALIDparameters was supplied but is not a JSON array.
400HEADER_MEDIA_INVALIDheader_media is not an object, its type is not image, video or document, or its url is not a valid https link.
400BUTTON_PARAMETERS_INVALIDbutton_parameters is not an array of { index, value } objects, an index is outside 0-9 or repeated, a value is empty, not a string, number or boolean, or longer than 2000 characters, or a type is not "url" or "quick_reply".
403CLIENT_BLOCKEDThe recipient is blocked by your business.
424WA_NOT_CONNECTEDYour business has not finished connecting WhatsApp.
400ACTION_TYPE_REQUIREDactionType is missing or empty on a /send-action request.
400ACTION_TYPE_UNKNOWNNo active configuration exists for that actionType. Your partner adds it on their partner page.
400ACTION_FIELD_MISSINGA field the action maps is absent or empty in the payload. The response lists each one in `missing_fields`.
400ACTION_FIELD_INVALIDA mapped field arrived but cannot be sent as-is - for example a value longer than WhatsApp accepts. The response lists each one in `invalid_fields` with the reason.
400PARTNER_NOT_CONFIGUREDThe business behind this API key is not managed by a partner, so it has no action configuration. Use /send-template.
400RECIPIENT_REQUIREDrecipient is missing or empty on a /send-action request.
400RECIPIENT_INVALIDThe recipient number could not be normalised to a valid form.
400TEMPLATE_SEND_FAILEDMeta rejected the send. The response includes Meta's error code (`meta_code`) and trace id (`meta_trace_id`) when available.
400INVALID_PATH_PARAMA collection id, row id or column key in a file download URL is malformed.
400NOT_A_FILE_COLUMNThe column in a file download URL is not a file column.
404ROW_NOT_FOUNDNo such row in that collection.
404CELL_EMPTYThat cell holds no file.
404FILE_UNAVAILABLEThe file this cell points at is no longer stored.
500FILE_LINK_FAILEDThe file exists but a download link could not be made. Retry shortly.
400TAGS_INVALIDtags is missing, is not an array, contains an entry that is not a non-empty string, or exceeds the limits of 100 names per request and 100 characters per name.
404CLIENT_NOT_FOUNDNo client with that phone number exists for your business. Client tag calls never create clients.
404TAG_NOT_FOUNDOne or more tag names do not exist in your business. Nothing was changed, and the response lists the unknown names in `missing_tags`.
500CLIENT_TAGS_FAILEDA client tags request failed on our side. The change may or may not have been applied. Retrying is safe, because add, replace and remove are all idempotent.