Docs · API and webhooks

API & webhooks — connect Craft to your CRM or website

How to control Craft with keyed requests on the Agency plan and receive events about new contacts, button clicks, payments and posts on your own server.

Updated: September 26, 2026

On the Agency plan you can connect Craft to your CRM, a client's website or any service of your own. Everything the MCP connector can do is available as a plain HTTPS request with a key: funnels, contacts, broadcasts, posts, carousels, images. Webhooks work the other way round: Craft sends an event to your server when a new contact appears, someone clicks a button, reaches a funnel goal, pays, or a post goes live.

How it works#

  • In Craft you create a key and choose what it may do: read only, or work with funnels, contacts, broadcasts, posts and so on.
  • Your server sends a request to https://craftopen.space/api/v1/tools/<method> with the key in a header. Method parameters go in the JSON request body.
  • Craft runs the method in your account — exactly as if you did it on the website or asked your AI through the MCP connector, with the same checks and limits.
  • For events you add a webhook — your server's address. Craft sends a signed request there and retries if your server doesn't answer.

What you need#

  • The Agency plan. Keys and webhooks can't be created on other plans.
  • A server or service that can send and receive HTTPS requests: your own code, Make, n8n, Zapier, or a CRM with incoming webhooks.

Step 1. Create a key#

Profile menu → API & webhooks (or Settings → API & webhooks) → New key.

  1. Give the key a name you'll recognise later: "HubSpot", "Client website".
  2. Choose what the key may do. A new key can only read by default. Everything else is switched on with checkboxes: funnels, contacts, broadcasts, posts, content, Content Factory, accounts. "Everything" opens all capabilities, including ones added later.
  3. Choose how the key publishes posts and broadcasts: - Drafts only — everything is saved as a draft and you send it yourself in Craft; - Scheduled — no sooner than in 10 minutes, so there's time to cancel; - Instantly — publishes and sends at the moment of the request.
  4. Set the daily limit on image spending and how many broadcasts per day the key may send from one bot.
  5. Click Create key and copy it right away.

🔴 The key is shown only once. Craft stores only its fingerprint and can't recover it. Lost it? Revoke it and create a new one. Never put the key into website page code or a mobile app — every visitor would see it. The key belongs on a server only.

You can change a key's permissions and settings at any time with "Settings" — changes take effect within a minute. "Revoke" turns the key off immediately.

Step 2. Your first request#

curl -X POST https://craftopen.space/api/v1/tools/list_funnels \
  -H "Authorization: Bearer craft_your_key" \
  -H "Content-Type: application/json" \
  -d '{}'

A successful response always looks like this — the data itself is in data:

{"ok": true, "data": {"funnels": [ ... ]}}

If something is wrong, you get "ok": false and a clear reason:

{"ok": false, "error": {"code": "funnel_not_found", "message": "Воронка не найдена."}}

⚠️ Error messages are currently written in Russian. Build your logic on error.code — it is stable and language-independent.

Available methods#

All methods and their parameters with descriptions:

  • in Craft: GET https://craftopen.space/api/v1/tools with your key — the list says which permission each method needs and whether this key may call it;
  • an OpenAPI description: craftopen.space/api/v1/openapi.json — Postman, Insomnia and client generators understand it.

The methods are the same as the MCP connector's. The most common ones:

Task Method Key permission
List funnels and their stats list_funnels, get_funnel_stats read
Find bot subscribers find_contacts read
Subscriber card get_contact read
Add or remove a tag tag_contacts contacts
Run a funnel for one person run_funnel_for_contact contacts, "Instantly" mode
Scheduled post list_publishing_accounts → schedule_post posts
Telegram broadcast save_broadcast_draft → send_broadcast_test → send_broadcast broadcasts
Carousel from your text create_carousel, render_carousel content
Image for credits generate_image content

GET https://craftopen.space/api/v1/me shows which key you're using, its permissions and how many requests are left today.

Retrying without duplicates#

If a request broke off and you don't know whether it went through, send it again with the same Idempotency-Key header. The post won't go out twice and an image won't be charged twice.

curl -X POST https://craftopen.space/api/v1/tools/schedule_post \
  -H "Authorization: Bearer craft_your_key" \
  -H "Idempotency-Key: post-2026-09-27-lime-1" \
  -H "Content-Type: application/json" \
  -d '{"account_ids": ["..."], "confirm_usernames": ["@lime_agency"],
       "carousel_id": "...", "caption": "Post text",
       "scheduled_at": "2026-09-27T19:00:00+03:00"}'

⚠️ Confirmations are part of the request. Where the website would ask "are you sure", the API expects a confirmation in the parameters: account @handles for a post (confirm_usernames), the exact number of recipients for a broadcast (confirm_recipients), the name of an active funnel you're editing, the price of a payment block. If the confirmation doesn't match what Craft sees, nothing is sent — the response tells you the correct value.

Errors and what to do#

Status Meaning
400 Invalid parameters — message says which
401 No key, wrong key, or the key was revoked
403 Plan without API (plan_required), the key lacks a permission (scope_required), or the publishing mode doesn't allow the action
404 Not found: funnel, contact, post or method
409 Data changed since you read it, or a confirmation didn't match — read again
429 Rate limit — wait as many seconds as the Retry-After header says
500, 503 Temporary failure — retry in a minute with the same Idempotency-Key

Limits#

  • 60 requests per minute and 10,000 per day per account — across all keys. The day is counted in Moscow time (UTC+3). Need more? Write to support; the limit is raised without changing your plan.
  • Webhooks don't count towards the limit.
  • Images spend credits at the same prices as on the website, and never above the daily limit you set for the key.
  • Broadcasts: no more than the chosen number per day per bot. A test to yourself with send_broadcast_test of the very same text is required before sending.

Webhooks#

API & webhooks → Add webhook: enter your server address (https:// only) and choose the events. Right after creation Craft shows the signing secret — save it, it isn't shown again (you can issue a new one with "New secret").

Test sends a ping event and shows what your server answered. Log lists recent deliveries: what was sent, whether it arrived, the response, and a "Retry" button.

Events#

Event When it arrives
contact.created Someone subscribed to a bot or wrote to your Instagram Direct for the first time
funnel.button_clicked Someone clicked a button in a funnel
funnel.goal_reached Someone reached the funnel goal
payment.succeeded A payment went through a funnel payment block
post.published A post went live on the social network

Example request body:

{
  "id": "6f1c…",
  "type": "contact.created",
  "created_at": "2026-09-26T09:04:11+00:00",
  "data": {
    "platform": "telegram",
    "contact_id": "…",
    "bot_username": "shop_bot",
    "first_name": "Anna",
    "username": "anna",
    "source": {"type": "link", "ref": "promo"}
  }
}

The event id stays the same across retries — use it to drop duplicates.

Verifying that a request came from Craft#

Every request has a Craft-Signature: t=<time>,v1=<signature> header. The signature is HMAC-SHA256 of the string <time>.<request body> with your secret. Compute it and compare:

import hmac, hashlib, time

def craft_signature_ok(header: str, body: bytes, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:   # older than 5 minutes — reject
        return False
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
const crypto = require("crypto");

function craftSignatureOk(header, rawBody, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

⚠️ The signature is computed over the raw request body — exactly the bytes that arrived. If your framework parses the JSON and re-serialises it first, the signature won't match.

Retries#

Craft waits up to 10 seconds for a response. Any 2xx response counts as delivered. Otherwise Craft retries after 1 minute, 5 minutes, 30 minutes, 2, 6, 12 and 24 hours — 8 attempts in total, after which the event is marked "failed" (you can resend it from the log).

🔴 After 50 failed deliveries in a row the webhook is turned off so it doesn't keep hitting a dead address. Fix your server and click "Turn on" — the counter resets. Events that happened while the webhook was off are not sent.

Answer quickly and do slow work after responding: if your server takes longer than 10 seconds, Craft counts the attempt as failed and sends the event again.

If you leave the Agency plan#

Keys stop working within a minute of the plan change, and webhooks stop sending. Keys and webhooks themselves aren't deleted: come back to Agency and everything works with the same settings.

Not available yet#

  • Ready-made client libraries — use the OpenAPI description.
  • Creating keys and webhooks through the API itself — only on the API & webhooks page.
  • "Sign in with Craft" for third-party apps: a key only gives access to your own account.
Didn't find an answer?Message us on Telegram or email [email protected] — we'll reply and extend the article.
Message on Telegram