2026-10-12 · 18 min read · Developer guide
Your website already knows when an order is paid, a booking is confirmed or a payment fails. This guide shows how to send that event to Insta AI 365 from your own backend and have one of your approved WhatsApp templates go out automatically — no plugin, on any stack that can make a signed HTTPS request.
1. What this does
Custom Website Integration is an HTTP endpoint plus an automation engine. Your backend posts a signed JSON event. Insta AI 365stores it, matches it against the automations you have switched on, fills your template’s placeholders from the event, and sends the message through your own connected WhatsApp Business Account.
Three things are yours: the credentials, the event you send, and the automation that decides what happens. Queueing, de-duplication, retries and the policy checks are handled for you.
202 means the event is stored durably. Whether a WhatsApp message follows depends on an automation being enabled, the template still being approved, the mapped fields being present, and the consent and policy checks passing. The execution log says which of those happened — this guide will not pretend the two are the same thing.2. Who it is for
The event vocabulary is deliberately not ecommerce-only. The same engine serves any business whose website knows something worth telling a customer:
| Business | Event | Message |
|---|---|---|
| Ecommerce | order.paid | Order confirmation with the order number and total |
| Travel agency | booking.created | Itinerary confirmed, with the reference and date |
| Clinic or dental practice | appointment.rescheduled | The new time, and who the patient is seeing |
| Education | lead.created | Application received, and the next step |
| Real estate | appointment.booked | Viewing confirmed, with the address and the agent |
| Professional services | payment.succeeded | A receipt with the reference and amount |
| Any subscription business | payment.failed | A payment problem the customer can actually act on |
3. Prerequisites
Four things, before the first request can succeed:
- An Insta AI 365 account on a plan that includes integrations or the communication hub. The WhatsApp Business API plans include both, so a WhatsApp-only customer is not locked out.
- A connected WhatsApp Business Account. An automation cannot be switched on until the website is bound to one of your connected numbers.
- At least one approved template. Templates are approved by Meta, not by us. A draft, pending, rejected or paused template cannot be selected.
- Server-side access to your website. The signing secret must never reach a browser, so the event has to be sent from your backend.
4. Quick start
In Insta AI 365, open Integrations → Custom Website and press Connect website. Give it a name and the domain — just the host, no https:// and no path.
You are shown an API key and a signing secret. Copy both now. The server keeps only a SHA-256 hash of the key and encrypted ciphertext of the secret, so neither can be shown a second time; rotating is the only way to get new ones.
export INSTAAI_API_KEY="iak_…"
export INSTAAI_SIGNING_SECRET="iss_…" # server only, never in a browser bundleThen send a test event using the snippet on the Developer setup tab — it is generated from the same contract the endpoint validates against, so a sample the server would reject cannot be published there. The event appears under Events within a second or two, with its id and processing status.
5. Authentication
Two separate credentials, because they fail differently. The API key identifies the integration; the signing secret proves the body. A key leaked from a log or a proxy cannot forge an event on its own.
| Credential | Shape | Sent as | Stored as |
|---|---|---|---|
| API key | iak_ + 32 random bytes, base64url | x-instaai-key | A SHA-256 hash, nothing else |
| Signing secret | iss_ + 32 random bytes, base64url | Never sent — it computes the signature | Encrypted at rest |
The tenant is resolved from the API key alone. If your payload carries an organisation or tenant id, it is ignored — a caller that could name its own tenant would be the whole isolation story undone.
6. Request signing
HMAC-SHA256, hex, over the exact string <timestamp>.<raw request body>.
signature = hex( HMAC_SHA256( signing_secret, timestamp + "." + raw_body ) )| Rule | Value |
|---|---|
| Algorithm | HMAC-SHA256 |
| Signed payload | <unix seconds> then . then the raw body bytes |
| Encoding | Hex. Case is not significant. |
| Signature header | x-instaai-signature |
| Timestamp header | x-instaai-timestamp — unix seconds |
| Replay window | ±300 seconds, in both directions |
| Comparison | Constant time |
The timestamp is inside the signed material deliberately: signing only the body would let anyone replay a captured request under a fresh timestamp. A delivery outside the window is refused whether or not its signature is valid, and the error says so — in practice it means the sending server’s clock is wrong.
7. API reference
POST /ecommerce/websites/events
One endpoint. It is the only route that takes your events, and it authenticates by API key rather than by session.
POST https://api.instaai365.com/ecommerce/websites/events
Content-Type: application/json
x-instaai-key: iak_…
x-instaai-timestamp: 1791900000
x-instaai-signature: 3f2a…{
"event_id": "order_10421_paid",
"event_type": "order.paid",
"occurred_at": "2026-10-12T10:30:00.000Z",
"data": {
"order": {
"id": "10421",
"number": "ORD-10421",
"total": 2499,
"currency": "INR",
"status": "paid"
},
"customer": {
"name": "Asha Menon",
"first_name": "Asha",
"phone": "919876543210",
"marketing_consent": "granted"
}
}
}| Field | Required | Notes |
|---|---|---|
event_id | Yes | Your own id. This is the idempotency key — make it deterministic, not a fresh UUID per attempt. |
event_type | Yes | One of the names in the catalogue below. Anything else is a 422. |
occurred_at | No | ISO 8601. Defaults to the time we received it. |
data.order | No | Order fields. Up to 100 line items. |
data.customer | No | Name, phone, email, marketing consent. The phone is the recipient. |
data.booking | No | Reference, start and previous start, service, provider, location. |
data.payment | No | Reference, amount, currency, method label, failure reason. |
eventType instead of event_type then fails loudly on your first test, rather than silently never firing in production.Status codes
| Code | Meaning | What to do |
|---|---|---|
202 | Accepted and stored durably | Nothing. Automations run shortly after. |
409 | This event_id was already accepted | Stop retrying. The response carries the stored event id. |
401 | The API key does not match an active integration | Check the key; check the website has not been revoked or disconnected. |
403 | The signature did not verify, or the timestamp is outside the window | Check what you signed; check the server clock. |
413 | The body is larger than 64 KB | Send only the fields your templates need. |
422 | Authentic, but the payload does not match the contract | Read the message — it names the field and the rule. |
429 | Over 600 requests in a minute for this integration | Back off, then retry with the same event_id. |
503 | A problem on our side | Retry with the same event_id. Nothing was stored. |
Every response, refusals included, carries a correlationId. Quote it in a support request; it is also the one field worth logging. A 202 looks like this:
{
"accepted": true,
"eventId": "4d3f…",
"automationsMatched": 1,
"correlationId": "9b1c…"
}automationsMatched is worth watching during setup: zero means the event was stored and no enabled automation wanted it, so no message will follow. A 409 carries duplicate: true and the stored eventId.
Limits
| Limit | Value |
|---|---|
| Request body | 64 KB |
| Rate | 600 requests per minute, per integration |
| Replay window | ±300 seconds |
| Line items per order | 100 |
Those are the figures the shipped code enforces. There is no published per-day quota on this endpoint; your WhatsApp sending volume is governed separately, by your plan and by Meta’s own messaging limits for your number.
8. Event catalogue
These are the event names the endpoint accepts. Each one is a trigger you can choose in the automation builder.
| Event | Means |
|---|---|
customer.created | Somebody registered an account |
lead.created | An enquiry or contact form was submitted |
contact.created | A contact was added to your own system |
order.created | An order was placed, before payment |
order.paid | Payment for an order cleared |
order.fulfilled | An order shipped or was completed |
order.cancelled | An order was cancelled |
order.refunded | An order was refunded |
booking.created | A booking was confirmed |
booking.cancelled | A booking was cancelled |
appointment.booked | An appointment was scheduled |
appointment.rescheduled | An appointment moved to a new time |
appointment.cancelled | An appointment was cancelled |
appointment.reminder | Remind someone of an upcoming appointment |
payment.succeeded | A payment cleared |
payment.failed | A payment attempt failed |
payment.refunded | A payment was refunded |
custom.event | Anything else — its meaning comes from the automation you point at it |
A booking event carries no order at all. Send the shape that fits what actually happened:
{
"event_id": "appt_9931_rescheduled_2",
"event_type": "appointment.rescheduled",
"data": {
"booking": {
"reference": "APPT-9931",
"starts_at": "2026-10-15T09:00:00Z",
"previous_starts_at": "2026-10-13T05:30:00Z",
"service": "Dental check-up",
"provider_name": "Dr Nair",
"location": "Andheri clinic"
},
"customer": { "name": "Ravi Kumar", "first_name": "Ravi", "phone": "919812345678" }
}
}9. Idempotency and retries
event_id is the idempotency key, scoped to your integration. Make it name the thing that happened — order_10421_paid, not a fresh UUID per attempt. Then a retry after a timeout returns 409 and your customer is not messaged twice.
Two guarantees are in play and both matter. The endpoint collapses duplicate deliveries onto one stored event. Separately, the engine keeps one logical send per automation and event, so even a replayed event cannot produce a second message. A retry you ask for from the dashboard is recorded as its own attempt, attributable to whoever requested it.
| Response | Retry? |
|---|---|
429 | Yes, after backing off. Same event_id. |
503 | Yes, with backoff. Same event_id. |
| Network error or timeout | Yes. Same event_id — a duplicate is safe. |
401, 403, 413, 422 | No. Each needs a change on your side first. |
409 | No. We already have it. |
10. Creating an automation
Open Integrations → Custom Website and press Create Automation. The builder walks through the website, the trigger event, an approved template and the variable mapping, then shows a preview before anything can be switched on.
Every automation is created paused, and changing its template or its mapping pauses it again. That is deliberate: an enabled automation whose configuration just changed would send the new shape to the next real customer with nobody having looked at it. Switching it on is a separate, explicit act — and when it cannot be switched on, it says which check failed rather than appearing to succeed.
All of it is decided server-side. The website, the template name, its category and its approval status are read from your own records rather than taken from whatever the browser submitted, and every route is scoped to your workspace and gated on the integrations permission.
11. Choosing a template
The picker lists only templates on your connected WhatsApp Business Account that are approved and active. Each row shows the name, language, category, how many values the body expects and a preview of the body text, so two similarly named templates can be told apart before anything is mapped.
| Template state | Selectable | Why |
|---|---|---|
| Approved and active | Yes | Meta will accept it |
| Pending review | No | Not approved yet — the first real event would fail |
| Rejected | No | Meta refused it. Edit and resubmit. |
| Paused or disabled | No | Meta has suspended it, usually for quality |
| Deleted at Meta after you selected it | No | The automation shows it as a blocking issue and will not send |
An empty list means one of two things, and the screen says which: either no WhatsApp Business Account is bound to this website yet — set that on the WhatsApptab — or you have no approved templates. You create and submit those in your WhatsApp template manager; approval is Meta’s decision and usually takes between a few minutes and a day. We cannot approve one on your behalf, and the builder offers no placeholder that pretends otherwise.
Templates are held once per workspace, keyed by name and language, not per connected number. So if you have more than one WhatsApp number, the picker shows every approved template in the workspace — make sure the one you choose is approved on the number this website actually sends from, or Meta will refuse the message.
12. Mapping variables
WhatsApp templates use positional placeholders — {{1}}, {{2}} — so the builder shows one row per placeholder the template declares, in order. You map each to a field chosen from a closed list of what the selected trigger actually carries.
{{1}} → customer.firstName e.g. "Ravi"
{{2}} → appointment.startsAt e.g. "15 Oct 2026, 2:30 pm"
{{3}} → booking.service e.g. "Dental check-up"The field list is an allowlist, not a path into your JSON. Arbitrary paths would let a template variable carry a field nobody intended to send into a message on somebody’s phone. A sample of what is available:
| Field | Renders as | Carried by |
|---|---|---|
store.name | Hexhealth Store | Every event |
customer.firstName | Asha | Any event with a customer |
order.number | ORD-10421 | Order events |
order.total | ₹2,499.00 | Order events |
shipping.trackingNumber | IN123456789 | order.fulfilled |
booking.reference | APPT-9931 | Booking and appointment events |
appointment.startsAt | 15 Oct 2026, 2:30 pm | Booking and appointment events |
payment.amount | ₹1,768.82 | Payment events |
payment.failureReason | The card was declined | payment.failed |
Dates render for a human in your business’s timezone and money groups for its currency — you send ISO 8601 and a number, and the engine formats them. Newlines and tabs inside a value are collapsed to single spaces, because WhatsApp rejects a parameter containing them: a message that fails at the provider is worse than one whose address lost a line break.
The mapping is validated before you can save, and again when the message is about to go out. A placeholder with no mapping, a field the chosen trigger cannot carry, and a template that wants more values than you have mapped are three distinct errors with three distinct messages.
When a mapped field is missing from the event
If the event does not carry a mapped field and you have set no fallback, the message is not sent, and the execution is logged as skipped with the reason. That is the deliberate choice: “Your order ORD-10421 has shipped, track it at ” cannot be taken back once a customer has read it. Set a fallback on any placeholder where a generic value is genuinely acceptable.
13. Category, consent and eligibility
Meta assigns every template a category, and the category decides what it may be used for. We enforce that when the message is sent, not only when you save the automation.
| Category | Use | Consent |
|---|---|---|
| UTILITY | Transactional messages the customer's own action triggered — order confirmations, appointment changes, receipts | No marketing opt-in required |
| MARKETING | Promotions, offers, re-engagement | A recorded opt-in for that number. Its absence blocks the send. |
| AUTHENTICATION | One-time passcodes, through Meta's authentication template flow | Use that flow — do not put codes in ordinary body variables |
Report consent on the event itself, with data.customer.marketing_consent set to granted, revoked or unknown. A revoked record is never overwritten by silence, and neither an unknown nor an absent consent will satisfy a MARKETING template: absence of evidence is not consent. Honour opt-outs in your own system too — a customer who replies STOP has told your number, not just your database.
The eligibility checks re-run at execution time: the automation must still be enabled, the template still approved and active, the WhatsApp connection still valid, the mapped values present, consent satisfied for the category, and your plan’s messaging entitlement not exhausted. Any failure is recorded against that execution with a readable reason — so a template Meta pauses an hour after you activate stops the automation, instead of producing a run of failed sends.
14. Node, PHP and Python
All three read credentials from the environment, serialise the body once and sign that exact string, set a real timeout, separate retryable failures from permanent ones, and log neither credentials nor customer data. The signature each one produces was checked against the server’s own verification before this was published, so all three interoperate byte for byte.
Node.js
const crypto = require("node:crypto");
const ENDPOINT = "https://api.instaai365.com/ecommerce/websites/events";
const API_KEY = process.env.INSTAAI_API_KEY;
const SIGNING_SECRET = process.env.INSTAAI_SIGNING_SECRET;
async function sendEvent(event) {
// Serialise once. Signing one string and sending another is the single
// commonest cause of a 403 here.
const body = JSON.stringify(event);
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(timestamp + "." + body)
.digest("hex");
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 15000);
try {
const res = await fetch(ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-instaai-key": API_KEY,
"x-instaai-timestamp": timestamp,
"x-instaai-signature": signature,
},
body,
signal: controller.signal,
});
const result = await res.json().catch(() => ({}));
// 202 accepted; 409 we already have this event_id. Both are success.
if (res.status === 202 || res.status === 409) return result;
// 429 and 5xx are worth sending again with the same event_id. 4xx are not.
if (res.status === 429 || res.status >= 500) {
throw Object.assign(new Error("instaai: retry later"), { retryable: true });
}
// Log the status and the correlation id. Never the body or the headers.
throw new Error("instaai: rejected " + res.status + " " + (result.correlationId || ""));
} finally {
clearTimeout(timer);
}
}
module.exports = { sendEvent };Next.js
Call it from a route handler, a server action or your payment webhook — never from a client component. Anything imported into a component ships to the browser, and a signing secret in a browser bundle is a public signing secret.
// app/api/orders/[id]/paid/route.ts — a server route, not a component.
import { NextResponse } from "next/server";
import { sendEvent } from "@/server/instaai"; // the module above
export async function POST(request: Request) {
const order = await confirmPayment(request);
await sendEvent({
event_id: `order_${order.id}_paid`,
event_type: "order.paid",
occurred_at: new Date().toISOString(),
data: {
order: { id: order.id, number: order.number, total: order.total, currency: "INR" },
customer: { name: order.customerName, phone: order.customerPhone },
},
});
return NextResponse.json({ ok: true });
}PHP / Laravel
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class InstaAiEvents
{
private const ENDPOINT = 'https://api.instaai365.com/ecommerce/websites/events';
public function send(array $event): array
{
$apiKey = config('services.instaai.key'); // INSTAAI_API_KEY
$secret = config('services.instaai.secret'); // INSTAAI_SIGNING_SECRET
// Serialise once, sign exactly what you send.
$body = json_encode($event, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$timestamp = (string) time();
$signature = hash_hmac('sha256', $timestamp . '.' . $body, $secret);
$response = Http::timeout(15)
->withHeaders([
'Content-Type' => 'application/json',
'x-instaai-key' => $apiKey,
'x-instaai-timestamp' => $timestamp,
'x-instaai-signature' => $signature,
])
->withBody($body, 'application/json')
->post(self::ENDPOINT);
$status = $response->status();
if ($status === 202 || $status === 409) {
return $response->json() ?? [];
}
if ($status === 429 || $status >= 500) {
throw new RuntimeException('instaai: retry later, status ' . $status);
}
throw new RuntimeException('instaai: rejected, status ' . $status);
}
}
// Dispatch from a queued job, so a slow HTTP call never holds up the response
// your own customer is waiting for:
//
// SendInstaAiEvent::dispatch($payload)->onQueue('webhooks');Python
import hashlib
import hmac
import json
import os
import time
import requests
ENDPOINT = "https://api.instaai365.com/ecommerce/websites/events"
API_KEY = os.environ["INSTAAI_API_KEY"]
SIGNING_SECRET = os.environ["INSTAAI_SIGNING_SECRET"]
class Retryable(RuntimeError):
"""Worth sending again with the same event_id."""
def send_event(event: dict) -> dict:
# Serialise once, sign exactly what you send, post those same bytes.
body = json.dumps(event, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
timestamp = str(int(time.time()))
signature = hmac.new(
SIGNING_SECRET.encode("utf-8"),
timestamp.encode("utf-8") + b"." + body,
hashlib.sha256,
).hexdigest()
response = requests.post(
ENDPOINT,
data=body,
headers={
"Content-Type": "application/json",
"x-instaai-key": API_KEY,
"x-instaai-timestamp": timestamp,
"x-instaai-signature": signature,
},
timeout=15,
)
if response.status_code in (202, 409):
return response.json()
if response.status_code == 429 or response.status_code >= 500:
raise Retryable(f"status {response.status_code}")
raise RuntimeError(f"rejected: status {response.status_code}")
# Call this from the place that already knows the booking is confirmed — a
# Django signal, a Celery task — and give it a deterministic event_id.Emit the event from the place in your backend that already knows the fact is true and final — after the gateway confirms the payment, after the booking row commits — and do it from a queue, so a slow HTTP call never delays the response your own customer is waiting for.
15. End to end
A clinic moves an appointment. The whole path, once:
- 1.The clinic’s Django backend saves the new time and calls
send_eventwithappointment.rescheduledand the idappt_9931_rescheduled_2. - 2. The endpoint checks the size, resolves the integration from the API key, applies the rate limit, verifies the signature and timestamp, validates the payload against the contract, stores the event, and answers
202with the stored event id. - 3. One execution is queued per enabled automation on that trigger.
- 4. The worker claims the execution, re-checks that the template is approved, that consent satisfies its category and that there is a recipient, then renders
["Ravi", "15 Oct 2026, 2:30 pm", "Dental check-up"]. - 5.The message goes out through the clinic’s own WhatsApp number, and the execution records the provider’s message id. Delivery and read arrive later, against that id.
16. Accepted, sent, delivered, read, failed
Five different facts. The log keeps them apart on purpose, because conflating the first with the fourth is how an integration comes to be believed to work when it does not.
| State | Means |
|---|---|
Accepted (202) | The event is stored. No message has been sent. |
| Queued | An automation matched; an execution is waiting for the worker. |
| Skipped | Deliberately not sent — paused, no consent, a missing mapped value, the template withdrawn. The reason is recorded. |
| Sent | WhatsApp accepted the message and returned a message id. |
| Delivered / Read | Reported later by WhatsApp, against that message id. |
| Failed | We tried and could not — a provider rejection, an exhausted plan limit, sending disabled. |
A preview in the builder renders example values and sends nothing. A test sendis a real WhatsApp message: it goes to the number you type out in full and confirm, it counts against your plan, and the result reports the provider’s message id — or the provider’s refusal. There is no third mode that pretends to send.
17. Logs and troubleshooting
The Events tab shows what arrived. The Message log shows what each automation did with it, with the provider message id, the values that were rendered and a masked recipient.
| Symptom | Likely cause | Fix |
|---|---|---|
401 on every request | Wrong key, or the website was revoked or disconnected | Check the key; reconnect the website if it was revoked |
403, signature | The body was serialised twice, or the wrong secret is in use | Sign the exact string you send; confirm the secret after any rotation |
403, timestamp | The sending server's clock is more than 5 minutes out | Run NTP on that host |
422 naming a field | The payload does not match the contract | Read the message — it names the field and the rule |
| Event accepted, no message | No enabled automation for that trigger | Switch the automation on, and clear any blocking issue it lists |
| Skipped: a missing value | The event did not carry a mapped field | Send the field, or set a fallback on that placeholder |
| Skipped: no consent | A MARKETING template with no opt-in recorded for that number | Report consent on the event, or use a Utility template |
| Skipped: template not usable | Meta paused, rejected or deleted the template | Fix it in your template manager, then re-enable the automation |
| Nothing at all under Events | The request never reached us | Check egress from your host, and that you are posting to the path above |
18. Security and data minimisation
- Keep the signing secret server-side. Never in browser JavaScript, a mobile app, a URL or a client-readable environment variable. If it has ever been in one, rotate it.
- Rotate from the dashboard. The previous key stops working immediately — a grace period in which a leaked key still works is a delay, not a rotation — so deploy the new values first.
- Revoke when a site is retired.That destroys the credentials and pauses the website’s automations. Your history is kept.
- Send the minimum.The contract has no field for a card number, a bank account, a password, an identity number, a diagnosis or a clinical note, and anything outside the declared fields does not survive validation. Do not route one-time passcodes through ordinary body variables; Meta’s authentication template flow exists for those.
- Remember where a variable ends up.It is rendered into a message on a personal phone, often readable on a lock screen and in a notification. “Your test result is positive” is not a template variable.
- Log the correlation id, not the payload.It is enough to trace a request without copying a customer’s name and number into your application logs.
19. Production checklist
- Credentials in your secret manager, not in source control.
event_iddeterministic per real-world event, so a retry de-duplicates instead of sending twice.- Events emitted from a queue or a background job, not inline in a request.
429,503and network errors retried with backoff;4xxnot retried.- NTP running on every host that signs a request.
- A Utility template for anything transactional.
- Consent reported on the event if you use MARKETING templates.
- A test send to your own number, and the preview read end to end, before you activate.
- Someone watching the message log through the first day of real traffic.
20. FAQ
Do I need a plugin?
No. This is an HTTP endpoint, so any backend that can make a signed HTTPS request works — WordPress, Laravel, Django, Rails, .NET, a Cloudflare Worker. Packaged platform connectors are a separate thing and are not required for this.
Will a retry message my customer twice?
Not if your event_id is deterministic. The endpoint de-duplicates the event, and the engine separately keeps one logical send per automation and event.
Can I send a template with an image header?
Not through this path — it fills body text variables only, so header media is never supplied. Choose a body-only template for an automation and use the campaign tools when the message needs header media.
Can I add conditions, like only orders above a value?
Not yet. An automation fires on its trigger for every event of that type. Filter on your own side — emit the event only when your condition holds — and the behaviour is the same. We would rather say that plainly than ship a condition field that is quietly ignored.
Who approves templates?
Meta. We read their verdict and refuse anything not approved and active. The WhatsApp Cloud API documentation covers templates, categories and messaging limits in full.
What happens if I disconnect WhatsApp?
Automations for that website show a blocking issue and cannot be switched on until a connection is selected again. Nothing is deleted.
Can one website drive several automations?
Yes, and several can share a trigger — an order.paid confirmation in English and another in Hindi, for instance. Each is queued, rendered and logged separately.
Connect your website
Create the integration, copy your credentials, send a signed test event, then map your first approved template. The Developer setup tab carries the snippet for your stack.
No matching features. Try “leads”, “AI” or “invoice”.